├── src ├── components │ ├── header │ │ ├── index.js │ │ └── style.css │ ├── input │ │ ├── index.js │ │ └── style.css │ ├── empty │ │ ├── style.css │ │ └── index.js │ ├── url │ │ ├── style.css │ │ └── index.js │ ├── list │ │ ├── style.css │ │ └── index.js │ ├── arrow │ │ ├── index.js │ │ └── style.css │ ├── message │ │ ├── style.css │ │ └── index.js │ ├── material-button │ │ ├── index.js │ │ └── style.css │ ├── icon-button │ │ ├── index.js │ │ └── style.css │ ├── footer │ │ ├── index.js │ │ └── style.css │ ├── sorter │ │ ├── index.js │ │ └── style.css │ ├── github │ │ └── index.js │ └── row │ │ ├── index.js │ │ └── style.css ├── containers │ ├── results │ │ ├── style.css │ │ ├── index.js │ │ ├── store.js │ │ └── getResults.js │ ├── app │ │ ├── index.js │ │ ├── style.css │ │ ├── store.js │ │ └── variables.css │ └── add │ │ ├── index.js │ │ ├── store.js │ │ └── style.css ├── lib │ ├── constants.js │ └── misc.js ├── public │ ├── popup.html │ ├── manifest.json │ └── img │ │ ├── github.svg │ │ ├── icon.svg │ │ ├── 32icon_dark.svg │ │ └── 32icon_light.svg └── search.js ├── nodemon.json ├── .env-template ├── prettier.config.js ├── screenshots └── screenshot-1.png ├── postcss.config.js ├── .editorconfig ├── web-ext-config.js ├── .babelrc ├── .eslintrc.js ├── CHANGELOG.md ├── index.js ├── README.md ├── webpack.config.js ├── .gitignore └── package.json /src/components/header/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/header/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/input/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/empty/style.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/input/style.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["*.test.js", "src/*", "dist/*"] 3 | } 4 | -------------------------------------------------------------------------------- /.env-template: -------------------------------------------------------------------------------- 1 | WEB_EXT_API_KEY="" 2 | WEB_EXT_API_SECRET="" 3 | PORT=3000 4 | -------------------------------------------------------------------------------- /src/containers/results/style.css: -------------------------------------------------------------------------------- 1 | .results { 2 | background: var(--dark-gray); 3 | } 4 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'es5', 4 | }; 5 | -------------------------------------------------------------------------------- /screenshots/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afk-mario/busca/HEAD/screenshots/screenshot-1.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer'); 2 | 3 | module.exports = { 4 | plugins: [autoprefixer], 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/url/style.css: -------------------------------------------------------------------------------- 1 | .url { 2 | display: flex; 3 | padding: var(--half-margin); 4 | width: 100%; 5 | flex-flow: column nowrap; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/constants.js: -------------------------------------------------------------------------------- 1 | export const ORDER_OPTIONS = ['VOTES', 'COMMENTS', 'DATE']; 2 | export const DEFAULT_SEARCH_URL = 'https://www.youtube.com/watch?v=QrR_gm6RqCo'; 3 | -------------------------------------------------------------------------------- /src/components/empty/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | 3 | import './style.css'; 4 | 5 | export default () => html` 6 |

7 |

8 | `; 9 | -------------------------------------------------------------------------------- /src/components/list/style.css: -------------------------------------------------------------------------------- 1 | .list { 2 | width: 100%; 3 | padding: 0; 4 | margin: 0; 5 | list-style: none; 6 | overflow-x: hidden; 7 | overflow-y: auto; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/arrow/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | 3 | import './style.css'; 4 | 5 | export default direction => html` 6 |
7 | `; 8 | -------------------------------------------------------------------------------- /src/components/message/style.css: -------------------------------------------------------------------------------- 1 | .message { 2 | padding: var(--margin); 3 | } 4 | 5 | .message .wrapper { 6 | padding: var(--margin); 7 | border: 2px solid var(--light-blue); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/material-button/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | import './style.css'; 3 | 4 | export default (icon, onclick) => html` 5 | 6 | ${icon} 7 | 8 | `; 9 | -------------------------------------------------------------------------------- /src/components/message/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | 3 | import './style.css'; 4 | 5 | export default message => html` 6 |
7 |
8 | ${message} 9 |
10 |
11 | `; 12 | -------------------------------------------------------------------------------- /src/components/url/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | 3 | import './style.css'; 4 | 5 | export default url => html` 6 |
7 |
8 |

url:

9 | ${url} 10 |
11 | 12 | `; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | iroot = 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 | # editorconfig-tools is unable to ignore longs strings or urls 11 | max_line_length = null 12 | -------------------------------------------------------------------------------- /src/public/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | busca 5 | 6 | 8 | 9 | 10 | 11 |

Loading ...

12 | 13 | 14 | -------------------------------------------------------------------------------- /web-ext-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | sourceDir: 'dist', 4 | build: { 5 | overwriteDest: true, 6 | }, 7 | run: { 8 | firefox: 'firefox-developer-edition', 9 | browserConsole: true, 10 | startUrl: [ 11 | 'about:debugging', 12 | 'https://www.youtube.com/watch?v=C4Uc-cztsJo', 13 | ], 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/material-button/style.css: -------------------------------------------------------------------------------- 1 | .material { 2 | color: var(light-gray); 3 | cursor: pointer; 4 | transition: all 0.2s ease; 5 | margin: 4px 0 0 0; 6 | } 7 | 8 | .material .material-icons { 9 | font-size: 20px; 10 | } 11 | 12 | .material:hover { 13 | color: var(--light-blue); 14 | transform: translate(0, -1px); 15 | box-shadow: var(--box-shadow-2); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/icon-button/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | 3 | import './style.css'; 4 | 5 | export default ({ active, icon, value, className, onclick }) => html` 6 | 14 | `; 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties", 5 | "@babel/plugin-syntax-export-default-from", 6 | "transform-export-extensions", 7 | "@babel/plugin-proposal-object-rest-spread" 8 | ], 9 | "compact" : false, 10 | "env": { 11 | "production": { 12 | "plugins": [ "transform-react-remove-prop-types" ] 13 | 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/search.js: -------------------------------------------------------------------------------- 1 | import choo from 'choo'; 2 | import devtools from 'choo-devtools'; 3 | 4 | import App from './containers/app'; 5 | import Store from './containers/app/store'; 6 | import ResultsStore from './containers/results/store'; 7 | 8 | const app = choo(); 9 | 10 | if (process.env.NODE_ENV !== 'production') { 11 | app.use(devtools()); 12 | } 13 | app.use(ResultsStore); 14 | app.use(Store); 15 | app.route('/*', App); 16 | app.mount('body'); 17 | -------------------------------------------------------------------------------- /src/components/list/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | 3 | import Row from '../row'; 4 | import Empty from '../empty'; 5 | import './style.css'; 6 | 7 | export default items => { 8 | const empty = Empty(); 9 | const rows = items.map(item => Row(item)); 10 | const content = items.length > 0 ? rows : empty; 11 | 12 | return html` 13 |
14 | 17 |
18 | `; 19 | }; 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb', 'prettier'], 3 | rules: { 4 | 'no-console': 0, 5 | }, 6 | globals: { 7 | browser: true, 8 | document: true, 9 | localStorage: true, 10 | chrome: true, 11 | fetch: true, 12 | Request: true, 13 | Headers: true, 14 | location: true, 15 | }, 16 | settings: { 17 | 'import/resolver': { 18 | webpack: { 19 | config: 'webpack.config.js', 20 | }, 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/footer/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | 3 | import github from '~components/github'; 4 | import './style.css'; 5 | 6 | export default () => html` 7 | 20 | `; 21 | -------------------------------------------------------------------------------- /src/components/icon-button/style.css: -------------------------------------------------------------------------------- 1 | .icon-button { 2 | color: currentcolor; 3 | transition: color 0.2s ease; 4 | cursor: pointer; 5 | padding: 5px; 6 | padding-left: var(--margin); 7 | padding-right: var(--margin); 8 | } 9 | 10 | .icon-button.-active.votes, 11 | .icon-button.votes:hover { 12 | color: var(--red); 13 | } 14 | 15 | .icon-button.-active.comments, 16 | .icon-button.comments:hover { 17 | color: var(--yellow); 18 | } 19 | 20 | .icon-button.-active.date, 21 | .icon-button.date:hover { 22 | color: var(--light-blue); 23 | } 24 | -------------------------------------------------------------------------------- /src/containers/app/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | 3 | import Results from '~containers/results'; 4 | 5 | import Footer from '~components/footer'; 6 | import Message from '~components/message'; 7 | 8 | import './variables.css'; 9 | import './style.css'; 10 | 11 | export default function app(state, emit) { 12 | const { url, message } = state; 13 | 14 | const messageBlank = typeof message === 'undefined' || !message; 15 | const content = messageBlank ? Results(state, emit) : Message(message); 16 | 17 | return html` 18 | 19 |
20 | ${content} 21 |
22 | ${Footer(url)} 23 | 24 | `; 25 | } 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.3.0] - 2019-02-19 10 | ### Added 11 | - Buttons for changing the order of the search results 12 | - Add web-ext config file for easier build toolchain 13 | - Add express config for easier debugging 14 | 15 | ## [1.2.1] - 2019-02-18 16 | ### Added 17 | - web-ext as a dependency 18 | - changelog file 19 | 20 | ### Changed 21 | - Webpack now cleans dist dir 22 | - upgrade dependencies 23 | - simplier build and start command 24 | -------------------------------------------------------------------------------- /src/containers/add/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | 3 | import './style.css'; 4 | 5 | export default ({ url, tags, updateUrl, updateTags, submit }) => html` 6 |
7 |
8 |
9 | 15 | 21 |
22 | save 23 |
24 |
25 |
26 |
27 | `; 28 | -------------------------------------------------------------------------------- /src/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "applications": { 3 | "gecko": { 4 | "id": "busca@ellugar.co", 5 | "strict_min_version": "42.0" 6 | } 7 | }, 8 | "manifest_version": 2, 9 | "name": "busca.afk", 10 | "description": "busca.afk extension", 11 | "author": "Mario Carballo Zama", 12 | "version": "1.3.0", 13 | "icons": { 14 | "16": "img/icon.svg", 15 | "19": "img/icon.svg", 16 | "38": "img/icon.svg", 17 | "48": "img/icon.svg", 18 | "128": "img/icon.svg", 19 | "256": "img/icon.svg", 20 | "512": "img/icon.svg" 21 | }, 22 | "browser_action": { 23 | "default_icon": "img/icon.svg", 24 | "default_popup": "popup.html" 25 | }, 26 | "permissions": ["https://www.reddit.com/", "webRequest", "tabs", "storage"] 27 | } 28 | -------------------------------------------------------------------------------- /src/public/img/github.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/arrow/style.css: -------------------------------------------------------------------------------- 1 | .arrow.up { 2 | width: 0; 3 | height: 0; 4 | border-left: 10px solid transparent; 5 | border-right: 10px solid transparent; 6 | 7 | border-bottom: 10px solid white; 8 | } 9 | 10 | .arrow.down { 11 | width: 0; 12 | height: 0; 13 | border-left: 10px solid transparent; 14 | border-right: 10px solid transparent; 15 | 16 | border-top: 10px solid white; 17 | } 18 | 19 | .arrow.right { 20 | width: 0; 21 | height: 0; 22 | border-top: 10px solid transparent; 23 | border-bottom: 10px solid transparent; 24 | 25 | border-left: 10px solid white; 26 | } 27 | 28 | .arrow.left { 29 | width: 0; 30 | height: 0; 31 | border-top: 10px solid transparent; 32 | border-bottom: 10px solid transparent; 33 | 34 | border-right: 10px solid white; 35 | } 36 | -------------------------------------------------------------------------------- /src/containers/app/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-family: var(--font); 4 | scrollbar-color: var(--medium-gray) var(--black); 5 | scrollbar-width: thin; 6 | } 7 | 8 | html, 9 | body { 10 | margin: 0; 11 | padding: 0; 12 | min-width: var(--popup-width); 13 | background: var(--dark-gray); 14 | color: var(--light-gray); 15 | overflow-x: hidden; 16 | 17 | max-width: var(--popup-width); 18 | max-height: var(--popup-height); 19 | } 20 | 21 | h1 { 22 | font-size: 1.5em; 23 | font-weight: 100; 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | h2 { 29 | font-size: 1em; 30 | font-weight: 100; 31 | margin: 0; 32 | padding: 0; 33 | } 34 | 35 | .wrapper { 36 | margin: auto; 37 | width: var(--popup-width); 38 | width: 100%; 39 | } 40 | 41 | main { 42 | padding-bottom: var(--footer-height); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/footer/style.css: -------------------------------------------------------------------------------- 1 | #footer { 2 | color: var(--white); 3 | position: fixed; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | 8 | height: var(--footer-height); 9 | background: var(--dark-gray); 10 | border-top: 2px solid var(--medium-gray); 11 | padding: var(--half-margin); 12 | 13 | max-width: var(--popup-width); 14 | 15 | display: flex; 16 | flex-flow: row; 17 | align-items: center; 18 | justify-content: center; 19 | } 20 | 21 | #footer .wrapper { 22 | display: flex; 23 | flex-flow: row nowrap; 24 | align-items: center; 25 | justify-content: space-between; 26 | } 27 | 28 | #footer a { 29 | color: currentcolor; 30 | transition: color 0.2s ease; 31 | width: 20px; 32 | } 33 | 34 | #footer a:hover { 35 | color: var(--light-blue); 36 | } 37 | 38 | #footer svg { 39 | width: 100%; 40 | height: auto; 41 | display: block; 42 | } 43 | -------------------------------------------------------------------------------- /src/containers/app/store.js: -------------------------------------------------------------------------------- 1 | export default ( 2 | state = { 3 | link: '', 4 | token: '', 5 | }, 6 | emitter 7 | ) => { 8 | const mState = state; 9 | mState.url = ''; 10 | mState.token = ''; 11 | 12 | emitter.on('DOMContentLoaded', () => { 13 | // restoreOptions(emitter); 14 | 15 | emitter.on('url:update', url => { 16 | mState.url = url; 17 | emitter.emit('render'); 18 | }); 19 | 20 | emitter.on('token:update', token => { 21 | mState.token = token; 22 | 23 | // if (typeof token === 'undefined' || !token) { 24 | // emitter.emit('message:update', 'no token'); 25 | // } 26 | 27 | emitter.emit('render'); 28 | }); 29 | 30 | emitter.on('message:update', message => { 31 | mState.message = message; 32 | emitter.emit('render'); 33 | }); 34 | 35 | emitter.emit('message:update', 'loading ...'); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/sorter/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | import IconButton from '~components/icon-button'; 3 | 4 | import './style.css'; 5 | 6 | const arrow = up => { 7 | if (up) 8 | return html` 9 | arrow_drop_down 10 | `; 11 | return html` 12 | arrow_drop_up 13 | `; 14 | }; 15 | 16 | export default ({ order, sorters, onclick }) => { 17 | const buttons = sorters.map(item => { 18 | const active = item.value === order; 19 | return html` 20 |
21 | ${IconButton({ 22 | ...item, 23 | active, 24 | onclick, 25 | })} 26 |
27 | `; 28 | }); 29 | 30 | return html` 31 |
32 | sort 33 |
${buttons}
34 |
35 | `; 36 | }; 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const dotenv = require('dotenv'); 3 | const express = require('express'); 4 | const webpack = require('webpack'); 5 | const middleware = require('webpack-dev-middleware'); 6 | const webpackConfig = require('./webpack.config.js'); 7 | 8 | const compiler = webpack({ ...webpackConfig, mode: 'development' }); 9 | 10 | dotenv.config(); 11 | 12 | const DEV = process.env.NODE_ENV !== 'production'; 13 | const PORT = process.env.PORT || 3000; 14 | const app = express(); 15 | 16 | app.use( 17 | middleware(compiler, { 18 | noInfo: true, 19 | writeToDisk: true, 20 | }) 21 | ); 22 | 23 | app.get('*', (req, res) => { 24 | res.sendFile(path.join(__dirname, 'dist', 'popup.html')); 25 | }); 26 | 27 | app.listen(PORT, err => { 28 | const info = `==> Listening on port ${PORT} Open up http://127.0.0.1:${PORT} dev:${DEV}`; 29 | if (err) { 30 | console.error(err); 31 | } 32 | console.info(info); 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/github/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | 3 | export default () => html` 4 | 10 | 15 | 16 | `; 17 | -------------------------------------------------------------------------------- /src/components/sorter/style.css: -------------------------------------------------------------------------------- 1 | .sort-list { 2 | display: flex; 3 | flex-flow: row; 4 | align-items: center; 5 | justify-content: space-between; 6 | border-bottom: 2px solid var(--medium-gray); 7 | } 8 | 9 | .sort-title { 10 | width: var(--side-width); 11 | 12 | display: flex; 13 | flex: 0 0 auto; 14 | flex-flow: column; 15 | align-items: center; 16 | } 17 | 18 | .sort-buttons { 19 | display: flex; 20 | flex: 1 0 auto; 21 | flex-flow: row; 22 | align-items: center; 23 | justify-content: space-between; 24 | color: white; 25 | border-left: 2px solid var(--medium-gray); 26 | } 27 | 28 | .sort-buttons button { 29 | background: transparent; 30 | border-radius: 0; 31 | border: none; 32 | } 33 | 34 | .sort-buttons .sort-button { 35 | flex: 1 0 auto; 36 | display: flex; 37 | flex-flow: row; 38 | align-items: center; 39 | justify-content: center; 40 | } 41 | 42 | .sort-buttons .sort-button:nth-child(2) { 43 | border-right: 2px solid var(--medium-gray); 44 | border-left: 2px solid var(--medium-gray); 45 | } 46 | -------------------------------------------------------------------------------- /src/containers/app/variables.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Fira+Mono:400,500,700'); 2 | 3 | :root { 4 | --font: 'Fira Mono', monospace; 5 | 6 | /* sizes */ 7 | --border-division: 2px; 8 | --popup-width: 450px; 9 | --popup-height: 500px; 10 | --side-width: 70px; 11 | --footer-height: 50px; 12 | 13 | /* colors */ 14 | 15 | --white: #ffffff; 16 | --black: #000000; 17 | --dark-gray: #0c0c0d; 18 | --medium-gray: #323234; 19 | --light-gray: #b2b2b2; 20 | --light-gray: #f2f2f2; 21 | 22 | --dark-blue: #185869; 23 | --light-blue: #98d1cf; 24 | --blue: #0080ff; 25 | 26 | --red: #c33126; 27 | --green: #2ab38d; 28 | --yellow: #f1b731; 29 | --magenta: #7a7e9d; 30 | 31 | /* margins */ 32 | 33 | --margin: 20px; 34 | --half-margin: calc(var(--margin) * 0.5); 35 | --double-margin: calc(var(--margin) * 2); 36 | 37 | /* shadows */ 38 | --text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2); 39 | --box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18); 40 | --box-shadow-2: 0 7px 14px rgba(50, 50, 93, 0.1), 41 | 0 3px 6px rgba(0, 0, 0, 0.08); 42 | } 43 | -------------------------------------------------------------------------------- /src/containers/results/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | 3 | import List from '~components/list'; 4 | import Sorter from '~components/sorter'; 5 | import { ORDER_OPTIONS } from '~lib/constants'; 6 | 7 | import { getSortedResults } from '~lib/misc'; 8 | 9 | import './style.css'; 10 | 11 | const sortbuttons = [ 12 | { 13 | className: 'votes', 14 | value: 'VOTES', 15 | icon: 'unfold_more', 16 | }, 17 | { 18 | className: 'comments', 19 | value: 'COMMENTS', 20 | icon: 'mode_comment', 21 | }, 22 | { 23 | className: 'date', 24 | value: 'DATE', 25 | icon: 'date_range', 26 | }, 27 | ]; 28 | 29 | export default function ResultsList(state, emit) { 30 | const { results = [], order = ORDER_OPTIONS[0] } = state; 31 | const sorted = getSortedResults(order, results); 32 | const onclick = value => { 33 | emit('order:set', value); 34 | }; 35 | 36 | return html` 37 |
38 | ${Sorter({ 39 | order, 40 | sorters: sortbuttons, 41 | onclick, 42 | })} 43 | ${List(sorted)} 44 |
45 | `; 46 | } 47 | -------------------------------------------------------------------------------- /src/containers/add/store.js: -------------------------------------------------------------------------------- 1 | import { getCurrentTabUrl } from '../../lib/misc'; 2 | 3 | export default ( 4 | state = { 5 | link: '', 6 | tags: '', 7 | token: '', 8 | }, 9 | emitter 10 | ) => { 11 | const mState = state; 12 | mState.url = ''; 13 | mState.tags = ''; 14 | mState.token = ''; 15 | 16 | emitter.on('DOMContentLoaded', () => { 17 | getCurrentTabUrl(url => { 18 | mState.url = url; 19 | emitter.emit('render'); 20 | }); 21 | 22 | // restoreOptions(emitter); 23 | 24 | emitter.on('url:update', url => { 25 | mState.url = url; 26 | emitter.emit('render'); 27 | }); 28 | 29 | emitter.on('tags:update', tags => { 30 | mState.tags = tags; 31 | emitter.emit('render'); 32 | }); 33 | 34 | emitter.on('token:update', token => { 35 | mState.token = token; 36 | 37 | if (typeof token === 'undefined' || !token) { 38 | emitter.emit('message:update', 'no token'); 39 | } 40 | 41 | emitter.emit('render'); 42 | }); 43 | 44 | emitter.on('message:update', message => { 45 | mState.message = message; 46 | emitter.emit('render'); 47 | }); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /src/public/img/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/public/img/32icon_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # busca 2 | 3 | ![logo](https://github.com/afk-mcz/busca.afk/blob/master/dist/img/32icon_dark.svg) 4 | 5 | A small web-extension to search the current tab on reddit. 6 | 7 | # Issues 8 | 9 | If the icon doesn't match the theme color make sure you have enable `svg.context-properties.content.enabled` on `about:config` 10 | 11 | Inspired by [Reddit Submission Finder](https://addons.mozilla.org/en-US/firefox/addon/reddit-submission-finder/) , [Socialite](https://addons.mozilla.org/en-US/firefox/addon/socialite/) and [Reddit Check](https://github.com/hsbakshi/reddit-check) 12 | 13 | # Screenshot 14 | 15 | ![Screenshot](https://github.com/afk-mcz/busca.afk/blob/master/screenshots/screenshot-1.png?raw=true) 16 | 17 | # Running and building from source 18 | 19 | Currently using webpack and web-ext to build the addon, if you want to try it yourself follow this steps: 20 | 21 | To install the project dependencies run: 22 | 23 | `yarn install` or `npm install` 24 | 25 | To start webpack on dev mode run: 26 | 27 | `yarn start` or `npm run start` 28 | 29 | To run the addon on firefox run: 30 | 31 | `web-ext run -s dist --firefox=PATH_TO_YOUR_FIREFOX_BIN --pre-install` 32 | 33 | To build the production webpack bundle run: 34 | 35 | `yarn build:prod` or `npm run build:prod` 36 | 37 | To build the addon zip run: 38 | 39 | `yarn build:fox` or `npm run build:fox` 40 | -------------------------------------------------------------------------------- /src/containers/results/store.js: -------------------------------------------------------------------------------- 1 | import { getCurrentTabUrl, handleErrors, removeDuplicatesBy } from '~lib/misc'; 2 | 3 | import getAllSubmissions from '~containers/results/getResults'; 4 | 5 | import { ORDER_OPTIONS } from '~lib/constants'; 6 | 7 | const store = ( 8 | state = { 9 | results: [], 10 | order: ORDER_OPTIONS[0], 11 | }, 12 | emitter 13 | ) => { 14 | const mState = state; 15 | mState.results = []; 16 | 17 | emitter.on('DOMContentLoaded', () => { 18 | emitter.on('results:got', results => { 19 | mState.results = results; 20 | emitter.emit('message:update', results.length > 0 ? '' : 'nothing found'); 21 | emitter.emit('render'); 22 | }); 23 | 24 | emitter.on('order:set', order => { 25 | mState.order = order; 26 | emitter.emit('render'); 27 | }); 28 | 29 | getCurrentTabUrl(url => { 30 | mState.url = url; 31 | // return; 32 | fetch('https://www.reddit.com/api/me.json') 33 | .then(handleErrors) 34 | .then(response => response.json()) 35 | .then(() => getAllSubmissions(url)) 36 | .then(results => { 37 | const filtered = removeDuplicatesBy(x => x.fullname, results); 38 | emitter.emit('results:got', filtered); 39 | }) 40 | .catch(error => console.error(error)); 41 | 42 | emitter.emit('render'); 43 | }); 44 | }); 45 | }; 46 | 47 | export default store; 48 | -------------------------------------------------------------------------------- /src/components/row/index.js: -------------------------------------------------------------------------------- 1 | import html from 'choo/html'; 2 | 3 | import Arrow from '../arrow'; 4 | import './style.css'; 5 | 6 | const dateFormat = { 7 | year: '2-digit', 8 | month: '2-digit', 9 | day: '2-digit', 10 | }; 11 | 12 | export default ({ 13 | link, 14 | fullname, 15 | title, 16 | score, 17 | age, 18 | date, 19 | comments, 20 | subreddit, 21 | likes, 22 | user, 23 | }) => html` 24 |
  • 25 |
    26 | ${Arrow('up')} 27 | ${score} 28 | ${Arrow('down')} 29 |
    30 | 36 |
    37 |

    ${title}

    38 |
    39 |
    40 | 41 | mode_comment 42 | ${comments} 43 | 44 | 45 | 46 | date_range 47 | 48 | ${date.toLocaleDateString(undefined, dateFormat)} 49 | 50 | 51 | r/${subreddit} 52 | u/${user} 53 |
    54 |
    55 | ${age} 56 | 57 | ${link} 58 |
  • 59 | `; 60 | -------------------------------------------------------------------------------- /src/public/img/32icon_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32icon_light 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | const output = path.resolve(__dirname, 'dist/'); 6 | 7 | module.exports = { 8 | devtool: 'cheap-module-source-map', 9 | entry: { search: './src/search.js' }, 10 | output: { 11 | filename: '[name].js', 12 | path: output, 13 | sourceMapFilename: '[name].map', 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.css$/, 19 | exclude: /node_modules/, 20 | use: [ 21 | { 22 | loader: 'style-loader', 23 | }, 24 | { 25 | loader: 'css-loader', 26 | options: { 27 | importLoaders: 1, 28 | }, 29 | }, 30 | { 31 | loader: 'postcss-loader', 32 | }, 33 | ], 34 | }, 35 | { 36 | test: /\.js$/, 37 | exclude: /(node_modules|bower_components)/, 38 | use: { 39 | loader: 'babel-loader', 40 | }, 41 | }, 42 | ], 43 | }, 44 | 45 | plugins: [ 46 | new CleanWebpackPlugin('dist'), 47 | new CopyWebpackPlugin([ 48 | { 49 | from: path.resolve(__dirname, './src/public'), 50 | to: path.resolve(__dirname, 'dist'), 51 | }, 52 | ]), 53 | ], 54 | 55 | resolve: { 56 | modules: ['node_modules'], 57 | alias: { 58 | '~lib': path.resolve('./src/lib/'), 59 | '~components': path.resolve('./src/components'), 60 | '~containers': path.resolve('./src/containers'), 61 | '~public': path.resolve('./src/public'), 62 | }, 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/vim,osx,linux 3 | 4 | ### Vim ### 5 | # swap 6 | [._]*.s[a-v][a-z] 7 | [._]*.sw[a-p] 8 | [._]s[a-v][a-z] 9 | [._]sw[a-p] 10 | # session 11 | Session.vim 12 | # temporary 13 | .netrwhist 14 | *~ 15 | # auto-generated tag files 16 | tags 17 | 18 | 19 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | # Thumbnails 27 | ._* 28 | # Files that might appear in the root of a volume 29 | .DocumentRevisions-V100 30 | .fseventsd 31 | .Spotlight-V100 32 | .TemporaryItems 33 | .Trashes 34 | .VolumeIcon.icns 35 | .com.apple.timemachine.donotpresent 36 | # Directories potentially created on remote AFP share 37 | .AppleDB 38 | .AppleDesktop 39 | Network Trash Folder 40 | Temporary Items 41 | .apdisk 42 | 43 | 44 | ### Linux ### 45 | 46 | # temporary files which can be created if a process still has a handle open of a deleted file 47 | .fuse_hidden* 48 | 49 | # KDE directory preferences 50 | .directory 51 | 52 | # Linux trash folder which might appear on any partition or disk 53 | .Trash-* 54 | 55 | # .nfs files are created when an open file is removed but is still being accessed 56 | .nfs* 57 | 58 | ### Node ### 59 | # Logs 60 | logs 61 | *.log 62 | npm-debug.log* 63 | 64 | # Runtime data 65 | pids 66 | *.pid 67 | *.seed 68 | *.pid.lock 69 | 70 | # Directory for instrumented libs generated by jscoverage/JSCover 71 | lib-cov 72 | 73 | # Coverage directory used by tools like istanbul 74 | coverage 75 | 76 | # nyc test coverage 77 | .nyc_output 78 | 79 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 80 | .grunt 81 | 82 | # node-waf configuration 83 | .lock-wscript 84 | 85 | # Compiled binary addons (http://nodejs.org/api/addons.html) 86 | build/Release 87 | 88 | # Dependency directories 89 | node_modules 90 | jspm_packages 91 | 92 | # Optional npm cache directory 93 | .npm 94 | 95 | # Optional eslint cache 96 | .eslintcache 97 | 98 | # Optional REPL history 99 | .node_repl_history 100 | 101 | # Output of 'npm pack' 102 | *.tgz 103 | 104 | # Yarn Integrity file 105 | .yarn-integrityA 106 | 107 | .web-ext-artifacts 108 | key.pem 109 | 110 | .env* 111 | !.env-template 112 | 113 | web-ext-artifacts 114 | dist 115 | .tern* 116 | -------------------------------------------------------------------------------- /src/containers/add/style.css: -------------------------------------------------------------------------------- 1 | .submit { 2 | background: var(--dark-gray); 3 | } 4 | 5 | form, 6 | .form { 7 | display: flex; 8 | padding: var(--half-margin); 9 | width: 100%; 10 | flex-flow: column nowrap; 11 | } 12 | 13 | input { 14 | width: 100%; 15 | height: auto; 16 | padding: var(--half-margin) 0 2px 0; 17 | font-size: 1em; 18 | background: transparent; 19 | border: none; 20 | box-shadow: none; 21 | color: var(--light-gray); 22 | border-bottom: 2px solid var(--medium-gray); 23 | transition: all 0.2s ease; 24 | color: var(--light-blue); 25 | } 26 | 27 | input[type='number']::-webkit-outer-spin-button, 28 | input[type='number']::-webkit-inner-spin-button { 29 | -webkit-appearance: none; 30 | margin: 0; 31 | } 32 | 33 | input[type='number'] { 34 | -moz-appearance: textfield; 35 | text-align: right; 36 | } 37 | 38 | input[type='color'] { 39 | height: 50px; 40 | border: none; 41 | } 42 | 43 | input:focus { 44 | /* color: var(--light-blue); */ 45 | color: var(--white); 46 | border-bottom-color: var(--light-blue); 47 | } 48 | 49 | form > *:not(:first-child) { 50 | margin: var(--margin) 0 0 0; 51 | } 52 | 53 | label { 54 | margin: 0 0 var(--half-margin) 0; 55 | display: block; 56 | color: var(--dark-blue); 57 | } 58 | 59 | button, 60 | .form .button, 61 | form .button { 62 | padding: var(--half-margin); 63 | } 64 | 65 | .fieldset { 66 | border: none; 67 | padding: 0; 68 | } 69 | 70 | .button { 71 | border: none; 72 | margin: var(--margin) 0 0 0; 73 | font-size: 1em; 74 | font-weight: 100; 75 | letter-spacing: 0.2em; 76 | cursor: pointer; 77 | display: flex; 78 | justify-content: center; 79 | align-items: center; 80 | text-align: center; 81 | text-decoration: none; 82 | transition: all 0.2s ease; 83 | text-shadow: none; 84 | cursor: pointer; 85 | 86 | color: var(--light-blue); 87 | border: 2px solid var(--medium-gray); 88 | text-shadow: var(--text-shadow); 89 | background: transparent; 90 | } 91 | 92 | .button:focus { 93 | outline: none; 94 | box-shadow: none; 95 | } 96 | 97 | .button:hover { 98 | transform: translate(0, -1px); 99 | box-shadow: var(--box-shadow-2); 100 | 101 | color: var(--white); 102 | border-color: var(--light-blue); 103 | } 104 | 105 | .button .marterial { 106 | opacity: 0.8; 107 | } 108 | -------------------------------------------------------------------------------- /src/components/row/style.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | flex-flow: row nowrap; 4 | border-bottom: 2px solid var(--medium-gray); 5 | font-size: 0.9em; 6 | align-items: center; 7 | } 8 | 9 | .row:last-child { 10 | border-bottom: 0; 11 | } 12 | 13 | .row .link, 14 | .row .age, 15 | .row .likes { 16 | display: none; 17 | } 18 | 19 | .row .-w-icon { 20 | display: flex; 21 | flex-flow: row; 22 | align-items: center; 23 | } 24 | 25 | .row .-w-icon .material-icons { 26 | font-size: 15px; 27 | margin: 0 5px 0 0; 28 | } 29 | 30 | .row .comments { 31 | color: var(--yellow); 32 | } 33 | 34 | .row .date { 35 | color: var(--light-blue); 36 | } 37 | .row .date span { 38 | padding-top: 3px; 39 | } 40 | 41 | .row .score { 42 | margin: calc(var(--half-margin) / 2) 0 calc(var(--half-margin) / 2) 0; 43 | } 44 | .row .subreddit { 45 | color: var(--magenta); 46 | display: block; 47 | } 48 | .row .title { 49 | width: 100%; 50 | margin: 0 0 var(--half-margin) 0; 51 | display: block; 52 | } 53 | 54 | .row .user { 55 | color: var(--dark-blue); 56 | } 57 | 58 | .row .score-container { 59 | width: var(--side-width); 60 | flex: 0 0 auto; 61 | height: 100%; 62 | display: flex; 63 | flex-flow: column; 64 | align-items: center; 65 | } 66 | 67 | .row .info-container { 68 | width: 100%; 69 | padding: var(--half-margin); 70 | display: flex; 71 | color: var(--white); 72 | text-decoration: none; 73 | font-size: 1em; 74 | flex-flow: column; 75 | align-items: flex-start; 76 | justify-content: space-between; 77 | padding-left: var(--half-margin); 78 | border-left: 2px solid var(--medium-gray); 79 | } 80 | 81 | .row .main-info-container { 82 | display: flex; 83 | width: 100%; 84 | flex-flow: row nowrap; 85 | } 86 | 87 | .row .additional-info-container { 88 | display: flex; 89 | flex-flow: row wrap; 90 | font-size: 0.9em; 91 | width: 100%; 92 | align-items: center; 93 | justify-content: flex-start; 94 | line-height: 1.6em; 95 | font-size: 0.8em; 96 | } 97 | 98 | .row .additional-info-container > *:not(:last-child) { 99 | margin-right: var(--margin); 100 | } 101 | 102 | .row .additional-info-container:last-child { 103 | margin-right: 0; 104 | } 105 | 106 | .row .arrow.up { 107 | border-bottom-color: var(--medium-gray); 108 | } 109 | 110 | .row .arrow.down { 111 | border-top-color: var(--medium-gray); 112 | } 113 | -------------------------------------------------------------------------------- /src/lib/misc.js: -------------------------------------------------------------------------------- 1 | import { ORDER_OPTIONS, DEFAULT_SEARCH_URL } from '~lib/constants'; 2 | 3 | export function restoreOptions(emitter) { 4 | function setCurrentChoice(result) { 5 | const { token = '' } = result; 6 | emitter.emit('token:update', token); 7 | } 8 | 9 | function onError(error) { 10 | console.error(`Error: ${error}`); 11 | } 12 | 13 | const getting = browser.storage.local.get('token'); 14 | getting.then(setCurrentChoice, onError); 15 | } 16 | 17 | export function sortByScore(a, b) { 18 | const sortA = a ? parseInt(a.score, 10) : 0; 19 | const sortB = b ? parseInt(b.score, 10) : 0; 20 | if (sortA < sortB) return 1; 21 | if (sortA > sortB) return -1; 22 | return 0; 23 | } 24 | 25 | export function sortByComments(a, b) { 26 | const sortA = a ? parseInt(a.comments, 10) : 0; 27 | const sortB = b ? parseInt(b.comments, 10) : 0; 28 | if (sortA < sortB) return 1; 29 | if (sortA > sortB) return -1; 30 | return 0; 31 | } 32 | 33 | export function sortByDate(a, b) { 34 | const sortA = a ? parseInt(a.age, 10) : 0; 35 | const sortB = b ? parseInt(b.age, 10) : 0; 36 | if (sortA < sortB) return 1; 37 | if (sortA > sortB) return -1; 38 | return 0; 39 | } 40 | 41 | export function getSortedResults(order, results) { 42 | switch (order) { 43 | case ORDER_OPTIONS[0]: 44 | return results.sort(sortByScore); 45 | case ORDER_OPTIONS[1]: 46 | return results.sort(sortByComments); 47 | case ORDER_OPTIONS[2]: 48 | return results.sort(sortByDate); 49 | default: 50 | return results.sort(sortByScore); 51 | } 52 | } 53 | 54 | export const flatten = list => 55 | list.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []); 56 | 57 | export function handleErrors(response) { 58 | if (!response.ok) { 59 | throw Error(response.statusText); 60 | } 61 | return response; 62 | } 63 | 64 | export function parseTags(_tags) { 65 | const tags = _tags.split(','); 66 | tags.push('fromBrowser'); 67 | return tags; 68 | } 69 | 70 | export function getCurrentTabUrl(callback) { 71 | // https://developer.chrome.com/extensions/tabs#method-query 72 | const queryInfo = { 73 | active: true, 74 | currentWindow: true, 75 | }; 76 | 77 | if (!chrome.tabs) callback(DEFAULT_SEARCH_URL); 78 | else 79 | chrome.tabs.query(queryInfo, tabs => { 80 | const tab = tabs[0]; 81 | const { url } = tab; 82 | // console.assert(typeof url == 'string', 'tab.url should be a string'); 83 | callback(url); 84 | }); 85 | } 86 | 87 | export function removeDuplicatesBy(keyFn, array) { 88 | const mySet = new Set(); 89 | return array.filter(x => { 90 | const key = keyFn(x); 91 | const isNew = !mySet.has(key); 92 | if (isNew) mySet.add(key); 93 | return isNew; 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "busca.afk", 3 | "version": "1.3.0", 4 | "description": "busca.afk extension to search links on reddit or more", 5 | "main": "js/script.js", 6 | "scripts": { 7 | "serve": "nodemon index.js", 8 | "start": "web-ext run", 9 | "start:macos": "web-ext run -p default --firefox=/Applications/FirefoxDeveloperEdition.app/Contents/MacOS/firefox-bin", 10 | "lint": "eslint src/**/*.js", 11 | "lint:web": "web-ext lint", 12 | "build": "webpack --watch --progress", 13 | "build:chrome": "crx pack dist -o build/ligo.crx", 14 | "build:fox": "webpack -p && web-ext build", 15 | "sign:fox": "env-cmd ./.env web-ext sign", 16 | "build:prod": "webpack -p", 17 | "precommit": "lint-staged" 18 | }, 19 | "repository": "https://github.com/afk-mcz/reddit-submission-finder", 20 | "keywords": [ 21 | "extension", 22 | "arlefreak", 23 | "afk", 24 | "ligo", 25 | "link" 26 | ], 27 | "author": "AFK", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/afk-mcz/busca/issues" 31 | }, 32 | "homepage": "https://github.com/afk-mcz/busca", 33 | "lint-staged": { 34 | "src/**/*.{js,json,css}": [ 35 | "prettier --single-quote --trailing-comma es5 --write", 36 | "git add" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "crx": "^4.0.1", 41 | "eslint": "^5.14.1", 42 | "eslint-config-airbnb": "^17.1.0", 43 | "eslint-import-resolver-webpack": "^0.11.0", 44 | "eslint-plugin-import": "^2.2.0", 45 | "eslint-plugin-jsx-a11y": "^6.0.2", 46 | "eslint-plugin-react": "^7.5.1", 47 | "express": "^4.16.4", 48 | "husky": "^1.3.1", 49 | "is": "^3.2.1", 50 | "lint-staged": "^8.1.4", 51 | "nodemon": "^1.18.10", 52 | "prettier": "1.16.4", 53 | "webpack-cli": "^3.2.3", 54 | "webpack-dev-middleware": "^3.6.0" 55 | }, 56 | "dependencies": { 57 | "@babel/cli": "^7.2.3", 58 | "@babel/core": "^7.3.3", 59 | "@babel/plugin-proposal-class-properties": "^7.3.3", 60 | "@babel/plugin-proposal-object-rest-spread": "^7.3.2", 61 | "@babel/plugin-syntax-export-default-from": "^7.2.0", 62 | "@babel/preset-env": "^7.3.1", 63 | "@babel/preset-react": "^7.0.0", 64 | "archiver": "^3.0.0", 65 | "autoprefixer": "^9.4.7", 66 | "babel-loader": "^8.0.5", 67 | "babel-plugin-transform-export-extensions": "^6.22.0", 68 | "choo": "^6.6.0", 69 | "choo-devtools": "^2.2.0", 70 | "clean-webpack-plugin": "^1.0.1", 71 | "copy-webpack-plugin": "^4.6.0", 72 | "css-loader": "^2.1.0", 73 | "dotenv": "^6.2.0", 74 | "env-cmd": "^8.0.2", 75 | "eslint-config-prettier": "^4.0.0", 76 | "postcss-loader": "^3.0.0", 77 | "precss": "^4.0.0", 78 | "style-loader": "^0.23.1", 79 | "urijs": "^1.19.0", 80 | "web-ext": "^2.9.3", 81 | "webpack": "^4.29.5" 82 | }, 83 | "peerDependencies": {} 84 | } 85 | -------------------------------------------------------------------------------- /src/containers/results/getResults.js: -------------------------------------------------------------------------------- 1 | // import Uri from 'urijs'; 2 | import { handleErrors, flatten } from '../../lib/misc'; 3 | 4 | function getYoutubeURLs(url) { 5 | let gotVidId = false; 6 | let videoId = ''; 7 | let urls = []; 8 | if (url.indexOf('v=') !== -1) { 9 | [, videoId] = url.split('v='); 10 | if (videoId !== '') gotVidId = true; 11 | const ampersandPosition = videoId.indexOf('&'); 12 | 13 | if (ampersandPosition !== -1) { 14 | videoId = videoId.substring(0, ampersandPosition); 15 | } 16 | 17 | // some youtube id's contain a dash at the start and reddit search interprets that as NOT 18 | // workaround is to search without the dash in the id 19 | if (videoId.indexOf('-') === 0) { 20 | videoId = videoId.substring(1); 21 | } 22 | } 23 | 24 | if (gotVidId) { 25 | const prefixes = [ 26 | 'http://www.youtube.com/watch?v=', 27 | 'https://www.youtube.com/watch?v=', 28 | 'http://www.youtu.be/', 29 | 'https://www.youtu.be/', 30 | ]; 31 | 32 | urls = prefixes 33 | .map(prefix => `${prefix}${videoId}`) 34 | .filter(item => item !== url); 35 | } 36 | 37 | return urls; 38 | } 39 | 40 | function constructUrls(URL) { 41 | let url = URL; 42 | if (url.indexOf('http') === -1) { 43 | return []; 44 | } 45 | 46 | let urls = [url]; 47 | if (url.indexOf('youtube.com') !== -1) { 48 | urls = urls.concat(getYoutubeURLs(url)); 49 | } else { 50 | // remove query string 51 | [url] = url.split('?'); 52 | // console.log(url); 53 | } 54 | 55 | if (url.startsWith('https')) { 56 | urls = urls.concat(url.replace('https', 'http')); 57 | } 58 | return urls; 59 | } 60 | 61 | function getAllURLVersions(URL) { 62 | let url = URL; 63 | 64 | // remove firefox reader 65 | if (url.indexOf('about:reader?url=') === 0) { 66 | url = decodeURIComponent(url.substring('about:reader?url='.length)); 67 | } 68 | 69 | const urls = constructUrls(url); 70 | const result = urls.map(item => { 71 | const query = encodeURIComponent(item); 72 | const redditUrl = `https://www.reddit.com/api/info.json?url=${query}`; 73 | return redditUrl; 74 | }); 75 | 76 | return result; 77 | } 78 | 79 | function handleResponse(response) { 80 | const now = new Date(); 81 | const timestamp = now.getTime(); 82 | const { 83 | data: { children }, 84 | } = response; 85 | console.log(children); 86 | 87 | const submissions = children.map(({ data }) => ({ 88 | fullname: data.name, 89 | link: `https://reddit.com${data.permalink}`, 90 | title: data.title, 91 | score: data.score.toString(), 92 | age: timestamp - data.created_utc * 1000, 93 | date: new Date(data.created_utc * 1000), 94 | comments: data.num_comments, 95 | subreddit: data.subreddit, 96 | likes: data.likes, 97 | user: data.author, 98 | })); 99 | 100 | return submissions; 101 | } 102 | 103 | function getURLSubmissions(path) { 104 | return fetch(path) 105 | .then(handleErrors) 106 | .then(response => response.json()) 107 | .then(result => handleResponse(result)) 108 | .catch(error => console.error(error)); 109 | } 110 | 111 | export default function getAllSubmissions(url) { 112 | const redditUrls = getAllURLVersions(url); 113 | const allPromises = redditUrls.map(redditUrl => getURLSubmissions(redditUrl)); 114 | 115 | // flatten the array 116 | return Promise.all(allPromises).then(results => flatten(results)); 117 | } 118 | --------------------------------------------------------------------------------