├── nodemon.json
├── .gitignore
├── src
├── client
│ ├── favicon.ico
│ ├── index.template.html
│ ├── components
│ │ ├── Loading.js
│ │ ├── ScrollUp.js
│ │ ├── Background.js
│ │ ├── Description.js
│ │ ├── NoData.js
│ │ ├── Notification.js
│ │ ├── Search.js
│ │ ├── Translate.js
│ │ ├── Image.js
│ │ └── Display.js
│ ├── index.js
│ ├── polyfills
│ │ ├── find.js
│ │ └── promise.js
│ ├── hooks
│ │ └── UseDebouncy.js
│ ├── containers
│ │ ├── Home.js
│ │ ├── Header.js
│ │ └── Gallery.js
│ └── reducer.js
├── assets
│ ├── fonts
│ │ ├── Inter-UI-Regular.woff
│ │ ├── Inter-UI-Regular.woff2
│ │ ├── fontawesome-webfont.eot
│ │ ├── fontawesome-webfont.ttf
│ │ ├── fontawesome-webfont.woff
│ │ └── fontawesome-webfont.woff2
│ ├── translations
│ │ ├── translations.js
│ │ ├── en.json
│ │ ├── pt.json
│ │ ├── fr.json
│ │ ├── de.json
│ │ ├── es.json
│ │ └── it.json
│ └── styles
│ │ ├── global.css
│ │ └── font-awesome.css
└── server
│ ├── utils
│ ├── algolia.js
│ ├── database.js
│ ├── crontab.js
│ └── middleware.js
│ ├── api
│ ├── images.routes.js
│ ├── images.model.js
│ └── images.controller.js
│ ├── index.js
│ ├── crons
│ ├── indexing.js
│ ├── cleaning.js
│ └── scraping.js
│ └── bots
│ ├── scraper.js
│ ├── negative.js
│ ├── kaboom.js
│ ├── twitter.js
│ ├── pexels.js
│ └── unsplash.js
├── .env.example
├── .babelrc
├── .eslintrc
├── README.md
├── webpack.config.js
└── package.json
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["src/server/"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 | database.json
4 | config.json
5 | .env
6 | todo
7 |
8 |
--------------------------------------------------------------------------------
/src/client/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quentindutot/motada-photos-browser/HEAD/src/client/favicon.ico
--------------------------------------------------------------------------------
/src/assets/fonts/Inter-UI-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quentindutot/motada-photos-browser/HEAD/src/assets/fonts/Inter-UI-Regular.woff
--------------------------------------------------------------------------------
/src/assets/fonts/Inter-UI-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quentindutot/motada-photos-browser/HEAD/src/assets/fonts/Inter-UI-Regular.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quentindutot/motada-photos-browser/HEAD/src/assets/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/src/assets/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quentindutot/motada-photos-browser/HEAD/src/assets/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quentindutot/motada-photos-browser/HEAD/src/assets/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/src/assets/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quentindutot/motada-photos-browser/HEAD/src/assets/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | ADMIN_KEY=""
2 | MONGODB_URI=""
3 | ALGOLIA_APPLICATION_ID=""
4 | ALGOLIA_SEARCH_KEY=""
5 | ALGOLIA_ADMIN_KEY=""
6 | SCRAPING_CRON="00 */2 * * *"
7 | CLEANING_CRON="00 */2 * * *"
8 | INDEXING_CRON="00 */2 * * *"
9 |
--------------------------------------------------------------------------------
/src/server/utils/algolia.js:
--------------------------------------------------------------------------------
1 | const algoliasearch = require('algoliasearch')
2 |
3 | const client = algoliasearch(process.env.ALGOLIA_APPLICATION_ID, process.env.ALGOLIA_ADMIN_KEY)
4 |
5 | module.exports = client.initIndex
6 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "@babel/plugin-proposal-class-properties",
8 | "@babel/plugin-transform-runtime"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/src/assets/translations/translations.js:
--------------------------------------------------------------------------------
1 | import de from './de.json'
2 | import en from './en.json'
3 | import es from './es.json'
4 | import fr from './fr.json'
5 | import it from './it.json'
6 | import pt from './pt.json'
7 |
8 | export default {
9 | de, en, es, fr, it, pt,
10 | }
11 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "env": {
4 | "es6": true,
5 | "browser": true,
6 | "node": true
7 | },
8 | "plugins": ["react"],
9 | "rules": {
10 | "no-console": "off",
11 | "react/jsx-filename-extension": "off",
12 | "no-plusplus": "off"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/server/api/images.routes.js:
--------------------------------------------------------------------------------
1 | const Images = require('./images.controller.js')
2 |
3 | module.exports = (router) => {
4 |
5 | router.get('/api/images/:id?', Images.find)
6 | router.patch('/api/images/:id?', Images.update)
7 | router.delete('/api/images/:id?', Images.delete)
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/client/index.template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Motada
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/client/components/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 |
4 | const Loading = ({ loading = false }) => (
5 | loading ? (
6 |
7 | ) : null
8 | )
9 |
10 | const mapState = state => ({
11 | loading: state.loading,
12 | })
13 |
14 | export default connect(mapState, null)(Loading)
15 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 |
3 | const database = require('./utils/database.js')
4 | const crontab = require('./utils/crontab.js')
5 | const api = require('./api/images.routes.js')
6 |
7 | const server = require('./utils/middleware.js')
8 | const port = process.env.PORT || 8080
9 |
10 | database()
11 | api(server)
12 | crontab()
13 |
14 | server.listen(port, () => console.log(`Listening on port ${port}!`))
15 |
--------------------------------------------------------------------------------
/src/assets/styles/global.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Inter UI';
3 | font-style: normal;
4 | font-weight: 400;
5 | src:
6 | local('Inter UI'),
7 | url(../fonts/Inter-UI-Regular.woff2) format('woff2'),
8 | url(../fonts/Inter-UI-Regular.woff) format('woff');
9 | }
10 |
11 | html {
12 | width: 100%;
13 | height: 100%;
14 | font-family: 'Inter UI', sans-serif;
15 | }
16 |
17 | body {
18 | width: 100%;
19 | margin: auto;
20 | padding: auto;
21 | text-align: center;
22 | }
23 |
--------------------------------------------------------------------------------
/src/client/components/ScrollUp.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { translate } from 'react-i18nify'
3 | import ScrollUpLib from 'react-scroll-up'
4 |
5 | const ScrollUp = () => (
6 |
7 |
12 |
13 | )
14 |
15 | export default ScrollUp
16 |
--------------------------------------------------------------------------------
/src/server/crons/indexing.js:
--------------------------------------------------------------------------------
1 | const algolia = require('../utils/algolia.js')
2 | const Images = require('../api/images.model.js')
3 |
4 | module.exports = async () => {
5 |
6 | const randomImages = await Images.getSamples(100)
7 |
8 | const ids = randomImages.map(({ _id }) => _id)
9 | const objects = randomImages.map(({ _id, title }) => ({ objectID: _id, title }))
10 |
11 | algolia('images')
12 | .saveObjects(objects)
13 | .then(console.log('images indexed on algolia:', ids))
14 | .catch(console.error)
15 | }
16 |
--------------------------------------------------------------------------------
/src/server/utils/database.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | const mongoose = require('mongoose')
3 |
4 | const url = process.env.MONGODB_URI || 'mongodb://localhost:27017/motada'
5 | const depreciations = {
6 | useNewUrlParser: true,
7 | useFindAndModify: false,
8 | useUnifiedTopology: true,
9 | useCreateIndex: true,
10 | }
11 |
12 | mongoose.Promise = global.Promise
13 |
14 | mongoose.connect(url, depreciations)
15 | .then(() => console.log('Database connected!'))
16 | .catch(() => console.log('Error connecting to database!'))
17 | }
18 |
--------------------------------------------------------------------------------
/src/server/utils/crontab.js:
--------------------------------------------------------------------------------
1 | const cron = require('node-cron')
2 |
3 | const scraping = require('../crons/scraping.js')
4 | const cleaning = require('../crons/cleaning.js')
5 | const indexing = require('../crons/indexing.js')
6 |
7 | module.exports = () => {
8 |
9 | // scraping cron
10 | cron.schedule(process.env.SCRAPING_CRON || '00 */2 * * *', scraping)
11 |
12 | // cleaning cron
13 | cron.schedule(process.env.CLEANING_CRON || '00 */2 * * *', cleaning)
14 |
15 | // indexing cron
16 | cron.schedule(process.env.INDEXING_CRON || '00 */2 * * *', indexing)
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/client/components/Background.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ParticleAnimation from 'react-particle-animation'
3 |
4 | const Background = () => {
5 |
6 | const isIE = /MSIE|Trident/.test(window.navigator.userAgent)
7 |
8 | return isIE ? null : (
9 |
16 | )
17 | }
18 |
19 | export default Background
20 |
--------------------------------------------------------------------------------
/src/client/components/Description.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Description = () => (
4 |
5 | {['unsplash.com', 'pexels.com', 'negativespace.co', 'kaboompics.com'].map(source => (
6 |
13 | {source}
14 |
15 | ))}
16 |
17 | )
18 |
19 | export default Description
20 |
--------------------------------------------------------------------------------
/src/server/utils/middleware.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const express = require('express')
3 | const bodyParser = require('body-parser')
4 | const cleanUrl = require('ssl-express-www')
5 |
6 | const server = express()
7 |
8 | server.use(cleanUrl)
9 |
10 | server.use(express.static(path.join(__dirname, '../../../dist')))
11 |
12 | server.use(bodyParser.urlencoded({ extended: true }))
13 | server.use(bodyParser.json())
14 |
15 | server.use((req, res, next) => {
16 | res.header('Access-Control-Allow-Origin', 'http://localhost:3000')
17 | next()
18 | })
19 |
20 | server.get('/', (req, res) => {
21 | res.sendFile(path.join(__dirname, '../../../dist', 'index.html'))
22 | })
23 |
24 | module.exports = server
25 |
--------------------------------------------------------------------------------
/src/server/api/images.model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const algolia = require('../utils/algolia.js')
3 |
4 | const schema = mongoose.Schema({
5 | url: { type: String, default: '' },
6 | title: { type: String, default: '' },
7 | source: { type: String, default: '' },
8 | tags: { type: [String], default: [], text: true },
9 | click: { type: Number, default: 0 },
10 | }, {
11 | timestamps: true
12 | })
13 |
14 | schema.statics.getSamples = function(size) {
15 | return this.aggregate([{ $sample: { size: Number(size) || 1 } }])
16 | }
17 |
18 | schema.statics.cleanDelete = function(id) {
19 | algolia('images').deleteObject(id)
20 | return this.findByIdAndRemove(id)
21 | }
22 |
23 | module.exports = mongoose.model('images', schema)
24 |
--------------------------------------------------------------------------------
/src/client/components/NoData.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { translate } from 'react-i18nify'
4 |
5 | const NoData = ({ search = '', loading = false }) => (
6 | loading ? null : (
7 |
8 |
9 |
10 | {translate('errors.no_results')}
11 |
12 |
13 | {search && localStorage.getItem('motada_language') !== 'en' && (
14 |
15 | {translate('errors.no_results_tip')}
16 |
17 | )}
18 |
19 |
20 | )
21 | )
22 |
23 | const mapState = state => ({
24 | search: state.search,
25 | loading: state.loading,
26 | })
27 |
28 | export default connect(mapState, null)(NoData)
29 |
--------------------------------------------------------------------------------
/src/assets/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "code": "en",
3 | "flag": "GB",
4 | "language": "english",
5 | "header": {
6 | "title": "Over %{count} free and high-res images",
7 | "default_title": "Over thousands free and high-res images"
8 | },
9 | "tooltips": {
10 | "search": "Search here...",
11 | "clear_search": "Clear",
12 | "translations": "Translations",
13 | "keywords": "Related keywords",
14 | "download": "Download",
15 | "save": "Save",
16 | "copy": "Copy",
17 | "close": "Close",
18 | "scroll_to_top": "Scroll to top"
19 | },
20 | "errors": {
21 | "no_translation": "Oops no translation available !",
22 | "no_results": "Oops no results !",
23 | "no_results_tip": "Sorry, the search engine only works in English for now...",
24 | "unknow": "Oops an error has occurred !"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/assets/translations/pt.json:
--------------------------------------------------------------------------------
1 | {
2 | "code": "pt",
3 | "flag": "PT",
4 | "language": "português",
5 | "header": {
6 | "title": "Mais de %{count} imagens gratuitas e de alta resolução",
7 | "default_title": "Mais de milhares de imagens gratuitas e de alta resolução"
8 | },
9 | "tooltips": {
10 | "search": "Procure aqui...",
11 | "clear_search": "Limpar",
12 | "translations": "Traduções",
13 | "keywords": "Palavras-chave relacionadas",
14 | "download": "Baixar",
15 | "save": "Salvar",
16 | "copy": "Copiar",
17 | "close": "Fechar",
18 | "scroll_to_top": "Rolar para cima"
19 | },
20 | "errors": {
21 | "no_translation": "Ups ! sem tradução disponível !",
22 | "no_results": "Ups ! sem resultados !",
23 | "no_results_tip": "Desculpe, o motor de busca só funciona em inglês por enquanto...",
24 | "unknow": "Ups ! Há um erro !"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/assets/translations/fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "code": "fr",
3 | "flag": "FR",
4 | "language": "français",
5 | "header": {
6 | "title": "Plus de %{count} images libres et gratuites",
7 | "default_title": "Plusieurs milliers d'images libres et gratuites"
8 | },
9 | "tooltips": {
10 | "search": "Rechercher ici...",
11 | "clear_search": "Effacer",
12 | "translations": "Traductions",
13 | "keywords": "Mots-clés liés",
14 | "download": "Télécharger",
15 | "save": "Enregistrer",
16 | "copy": "Copier",
17 | "close": "Fermer",
18 | "scroll_to_top": "Défiler vers le haut"
19 | },
20 | "errors": {
21 | "no_translation": "Oups aucune traduction disponible !",
22 | "no_results": "Oups aucun résultat !",
23 | "no_results_tip": "Désolé actuellement le moteur de recherche ne fonctionne qu'en anglais...",
24 | "unknow": "Oups une erreur est survenue !"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/assets/translations/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "code": "de",
3 | "flag": "DE",
4 | "language": "deutsch",
5 | "header": {
6 | "title": "Über %{count} kostenlose und hochauflösende Bilder",
7 | "default_title": "Über tausende kostenlose und hochauflösende Bilder"
8 | },
9 | "tooltips": {
10 | "search": "Suche hier...",
11 | "clear_search": "Löschen",
12 | "translations": "Übersetzungen",
13 | "keywords": "Verwandte Stichwörter",
14 | "download": "Herunterladen",
15 | "save": "Speichern",
16 | "copy": "Kopieren",
17 | "close": "Schließen",
18 | "scroll_to_top": "Scrolle nach oben"
19 | },
20 | "errors": {
21 | "no_translation": "Ups keine Übersetzung verfügbar !",
22 | "no_results": "Hoppla, keine Ergebnisse !",
23 | "no_results_tip": "Entschuldigung, die Suchmaschine funktioniert momentan nur auf Englisch...",
24 | "unknow": "Ups ein Fehler ist aufgetreten !"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/assets/translations/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "code": "es",
3 | "flag": "ES",
4 | "language": "español",
5 | "header": {
6 | "title": "Más de %{count} imágenes gratuitas y de alta resolución",
7 | "default_title": "Más de miles de imágenes gratuitas y de alta resolución"
8 | },
9 | "tooltips": {
10 | "search": "Buscar aquí...",
11 | "clear_search": "Limpiar",
12 | "translations": "Traducciones",
13 | "keywords": "Palabras claves relacionadas",
14 | "download": "Descargar",
15 | "save": "Guardar",
16 | "copy": "Copiar",
17 | "close": "Cerrar",
18 | "scroll_to_top": "Vuelve al comienzo"
19 | },
20 | "errors": {
21 | "no_translation": "¡ Uy, ninguna traducción disponible !",
22 | "no_results": "¡ Uy no hay resultados !",
23 | "no_results_tip": "Lo sentimos, el motor de búsqueda solo funciona en inglés por ahora...",
24 | "unknow": "¡ Uy, ha ocurrido un error !"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/assets/translations/it.json:
--------------------------------------------------------------------------------
1 | {
2 | "code": "it",
3 | "flag": "IT",
4 | "language": "italiano",
5 | "header": {
6 | "title": "Oltre %{count} immagini gratuite e ad alta risoluzione",
7 | "default_title": "Oltre mille immagini gratuito e ad alta risoluzione"
8 | },
9 | "tooltips": {
10 | "search": "Cerca qui...",
11 | "clear_search": "Cancellare",
12 | "translations": "Traduzioni",
13 | "keywords": "Parole chiave correlate",
14 | "download": "Scaricare",
15 | "save": "Registrare",
16 | "copy": "Copia",
17 | "close": "Chiudere",
18 | "scroll_to_top": "Scorri in alto"
19 | },
20 | "errors": {
21 | "no_translation": "Oops nessuna traduzione disponibile !",
22 | "no_results": "Spiacenti alcun risultato !",
23 | "no_results_tip": "Siamo spiacenti, il motore di ricerca funziona solo in inglese per ora...",
24 | "unknow": "Spiacenti, si è verificato un errore !"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/server/crons/cleaning.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const Images = require('../api/images.model.js')
3 |
4 | module.exports = async () => {
5 |
6 | const randomImages = await Images.getSamples(100)
7 |
8 | randomImages.forEach((randomImage) => {
9 | axios.head(randomImage.url).then(async () => {
10 |
11 | // delete duplicate images
12 | const similarImages = await Images.find({ url: randomImage.url })
13 | const duplicateImages = similarImages.filter(similarImage => similarImage._id.toString() != randomImage._id.toString())
14 | duplicateImages.forEach(async (duplicateImage) => {
15 | await Images.cleanDelete(duplicateImage._id)
16 | console.log('duplicate image deleted:', duplicateImage._id)
17 | })
18 |
19 | }).catch(async () => {
20 |
21 | // delete unreachable image
22 | await Images.cleanDelete(randomImage._id)
23 | console.log('unreachable image deleted:', randomImage._id)
24 |
25 | })
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/src/client/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { connect } from 'react-redux'
3 | import { updateNotification } from '../reducer'
4 | import Slide from 'react-reveal/Slide'
5 |
6 | const Notification = ({ notification = '', updateNotification = () => {} }) => {
7 |
8 | useEffect(() => {
9 | if (!notification) return
10 | const clearNotification = () => updateNotification('')
11 | setTimeout(clearNotification, 6000)
12 | return () => clearTimeout(clearNotification)
13 | }, [notification])
14 |
15 | return notification ? (
16 |
17 |
18 | {notification}
19 |
20 |
21 | ) : null
22 | }
23 |
24 | const mapState = state => ({
25 | notification: state.notification,
26 | })
27 |
28 | const mapDispatch = dispatch => ({
29 | updateNotification: notification => dispatch(updateNotification(notification)),
30 | })
31 |
32 | export default connect(mapState, mapDispatch)(Notification)
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # motada-photos-browser
2 | full-stack javascript single page appplication | [motada.cc](https://motada.cc)
3 |
4 | ## Concept
5 |
6 | Motada is a free and high-res images browser.
7 | Best images from best plateformes centralized on a single website with no account creation, license, credits...
8 |
9 | Daily new images from unsplash.com, pexels.com, pixabay.com, freerangestock.com, flickr.com, gratisography.com, freeimages.co.uk, picjumbo.com, morguefile.com, pikwizard.com.
10 |
11 | ## History
12 |
13 | 2018 preview:
14 |
15 |
16 |
17 | ## Stack
18 |
19 | #### `frontend`: [React](https://github.com/facebook/react) - [Redux](https://github.com/reduxjs/redux) - [TailwindCSS](https://github.com/tailwindlabs/tailwindcss) (previously [Material-UI](https://github.com/mui-org/material-ui)) - [Webpack](https://github.com/webpack/webpack)
20 |
21 | #### `backend`: [Node.js](https://nodejs.org/en/) - [Express](https://github.com/expressjs/express) - [MongoDB](https://github.com/mongodb/mongo) - [Puppeteer](https://github.com/GoogleChrome/puppeteer) - [Algolia](https://github.com/algolia)
22 |
23 | #### `boilerplate`: [simple-react-full-stack](https://github.com/crsandeep/simple-react-full-stack)
24 |
--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import ReactGA from 'react-ga'
4 | import { setTranslations, setLocale } from 'react-i18nify'
5 | import { Provider } from 'react-redux'
6 | import { createStore } from 'redux'
7 | import translations from '../assets/translations/translations'
8 | import Home from './containers/Home'
9 | import reducer from './reducer'
10 |
11 | // bundle needed styles (tree shaked with webpack)
12 | import '../assets/styles/font-awesome.css'
13 | import '../assets/styles/tailwind.css'
14 | import '../assets/styles/global.css'
15 |
16 | // bundle needed polyfills
17 | import './polyfills/promise'
18 | import './polyfills/find'
19 |
20 | // add supported locales
21 | setTranslations(translations)
22 |
23 | // extract and validate user language
24 | const detectedLanguage = (localStorage.getItem('motada_language') || navigator.language || navigator.userLanguage || 'en').substring(0,2)
25 | const validatedLanguage = translations[detectedLanguage] ? detectedLanguage : 'en'
26 | setLocale(validatedLanguage)
27 |
28 | // init google analytics tracking
29 | if(window.location.href.indexOf('localhost') === -1) {
30 | ReactGA.initialize('UA-127688890-1')
31 | ReactGA.pageview(window.location.pathname + window.location.search)
32 | }
33 |
34 | // render react application
35 | ReactDOM.render(
36 |
37 |
38 | ,
39 | document.getElementById('root'),
40 | )
41 |
--------------------------------------------------------------------------------
/src/client/polyfills/find.js:
--------------------------------------------------------------------------------
1 | // https://tc39.github.io/ecma262/#sec-array.prototype.find
2 | if (!Array.prototype.find) {
3 | Object.defineProperty(Array.prototype, 'find', {
4 | value: function(predicate) {
5 | // 1. Let O be ? ToObject(this value).
6 | if (this == null) {
7 | throw TypeError('"this" is null or not defined');
8 | }
9 |
10 | var o = Object(this);
11 |
12 | // 2. Let len be ? ToLength(? Get(O, "length")).
13 | var len = o.length >>> 0;
14 |
15 | // 3. If IsCallable(predicate) is false, throw a TypeError exception.
16 | if (typeof predicate !== 'function') {
17 | throw TypeError('predicate must be a function');
18 | }
19 |
20 | // 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
21 | var thisArg = arguments[1];
22 |
23 | // 5. Let k be 0.
24 | var k = 0;
25 |
26 | // 6. Repeat, while k < len
27 | while (k < len) {
28 | // a. Let Pk be ! ToString(k).
29 | // b. Let kValue be ? Get(O, Pk).
30 | // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
31 | // d. If testResult is true, return kValue.
32 | var kValue = o[k];
33 | if (predicate.call(thisArg, kValue, k, o)) {
34 | return kValue;
35 | }
36 | // e. Increase k by 1.
37 | k++;
38 | }
39 |
40 | // 7. Return undefined.
41 | return undefined;
42 | },
43 | configurable: true,
44 | writable: true
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/src/client/hooks/UseDebouncy.js:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from 'react'
2 |
3 | // 16 ms its time on 1 frame
4 | const FRAME_MS = 16
5 |
6 | /**
7 | * @param fn - Debounce callback.
8 | * @param wait - Number of milliseconds to delay.
9 | * @param deps - Array values that the debounce depends (like as useEffect).
10 | */
11 | const useDebouncy = (fn = () => {}, wait = 1000, deps = []) => {
12 | const defaultWait = wait || 0
13 | const defaultDeps = deps || []
14 | const rafId = useRef(0)
15 | const timeStart = useRef(0)
16 | const callback = useRef(fn)
17 | const isFirstRender = useRef(true)
18 |
19 | const renderFrame = useRef((timeNow) => {
20 | // call will be after the first frame - requires subtracting 16 ms for more accurate timing.
21 | timeStart.current = timeStart.current || timeNow - FRAME_MS
22 |
23 | // call next rAF if time is not up
24 | if (timeNow - timeStart.current < defaultWait) {
25 | rafId.current = requestAnimationFrame(renderFrame.current)
26 | return
27 | }
28 |
29 | callback.current()
30 | })
31 |
32 | // set new callback if it updated
33 | useEffect(() => {
34 | callback.current = fn
35 | }, [fn])
36 |
37 | // call update if deps changes
38 | useEffect(() => {
39 | if (isFirstRender.current) {
40 | isFirstRender.current = false
41 | return () => {}
42 | }
43 |
44 | timeStart.current = 0
45 | rafId.current = requestAnimationFrame(renderFrame.current)
46 |
47 | return () => {
48 | cancelAnimationFrame(rafId.current)
49 | }
50 | }, defaultDeps)
51 | }
52 |
53 | export default useDebouncy
54 |
--------------------------------------------------------------------------------
/src/server/api/images.controller.js:
--------------------------------------------------------------------------------
1 | const algolia = require('../utils/algolia.js')
2 | const Images = require('./images.model.js')
3 |
4 | exports.find = async (req, res) => {
5 | const { query } = req
6 | const { random, search } = query
7 | let result = false
8 |
9 | // api/images?count
10 | if (Object.prototype.hasOwnProperty.call(query, 'count')) {
11 | result = {
12 | count: await Images.estimatedDocumentCount()
13 | }
14 | }
15 |
16 | // api/images?random=3
17 | if (Object.prototype.hasOwnProperty.call(query, 'random')) {
18 | result = {
19 | random: await Images.getSamples(random)
20 | }
21 | }
22 |
23 | // api/images?search=beautiful car
24 | if (Object.prototype.hasOwnProperty.call(query, 'search')) {
25 | const { hits } = await algolia('images').search(search, { page: 0, hitsPerPage: 1000 })
26 | const ids = hits.map(({ objectID }) => objectID)
27 | result = {
28 | search: await Images.find({ _id: { $in: ids } })
29 | }
30 | }
31 |
32 | res.send(JSON.stringify(result))
33 | }
34 |
35 | exports.update = async (req, res) => {
36 | const { params, body } = req
37 | let result = false
38 |
39 | if (params.id) {
40 | const image = await Images.findByIdAndUpdate(params.id, body, { new: true })
41 | if (image) result = true
42 | }
43 |
44 | res.send(JSON.stringify(result))
45 | }
46 |
47 | exports.delete = async (req, res) => {
48 | const { params } = req
49 | let result = false
50 |
51 | if (params.id) {
52 | const image = await Images.cleanDelete(params.id)
53 | if (image) result = true
54 | }
55 |
56 | res.send(JSON.stringify(result))
57 | }
58 |
--------------------------------------------------------------------------------
/src/server/bots/scraper.js:
--------------------------------------------------------------------------------
1 | const Puppeteer = require("puppeteer")
2 | const Pexels = require("./pexels")
3 | const Unsplash = require("./unsplash")
4 | const Negative = require("./negative")
5 | const Kaboom = require("./kaboom")
6 |
7 | class Scraper {
8 |
9 | constructor() {
10 | this.config = {
11 | headless: true,
12 | options: ["--disable-gpu", "--no-sandbox", "--window-size=1024x768"],
13 | viewport: { "width": 1024, "height": 768 },
14 | header: { "Accept-Language": "en" },
15 | }
16 | }
17 |
18 | async start() {
19 | const { headless, options, viewport, header } = this.config
20 |
21 | this.browser = await Puppeteer.launch({
22 | headless: headless,
23 | args: options,
24 | defaultViewport: viewport,
25 | })
26 |
27 | this.page = await this.browser.newPage()
28 |
29 | await this.page.setExtraHTTPHeaders(header)
30 | await this.page.setViewport(viewport)
31 | // await this.page.setUserAgent(this.config.puppeteer.chrome_useragent === "" ? (await this.browser.userAgent()).replace("Headless", "") : this.config.puppeteer.chrome_useragent)
32 | }
33 |
34 | async api(flow) {
35 | switch (flow) {
36 | case 'unsplash':
37 | this.api = new Unsplash(this.page)
38 | break
39 | case 'negative':
40 | this.api = new Negative(this.page)
41 | break
42 | case 'kaboom':
43 | this.api = new Kaboom(this.page)
44 | break
45 | case 'pexels': default:
46 | this.api = new Pexels(this.page)
47 | break
48 | }
49 | }
50 |
51 | async stop() {
52 | await this.browser.newPage()
53 | await this.browser.close()
54 | }
55 |
56 | }
57 |
58 | module.exports = Scraper
--------------------------------------------------------------------------------
/src/client/components/Search.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { translate } from 'react-i18nify'
3 | import { connect } from 'react-redux'
4 | import useDebouncy from '../hooks/UseDebouncy'
5 | import { makeSearch } from '../reducer'
6 |
7 | const Search = ({ search = '', makeSearch = () => {} }) => {
8 |
9 | const [value, setValue] = useState(search)
10 | useDebouncy(
11 | () => makeSearch(value),
12 | 500,
13 | [value],
14 | )
15 |
16 | useEffect(() => {
17 | if (search == value) return
18 | setValue(search)
19 | }, [search])
20 |
21 | return (
22 |
39 | )
40 | }
41 |
42 | const mapState = state => ({
43 | search: state.search,
44 | })
45 |
46 | const mapDispatch = dispatch => ({
47 | makeSearch: search => dispatch(makeSearch(search)),
48 | })
49 |
50 | export default connect(mapState, mapDispatch)(Search)
51 |
--------------------------------------------------------------------------------
/src/server/crons/scraping.js:
--------------------------------------------------------------------------------
1 | const reboot = require('nodejs-system-reboot')
2 | const Scraper = require('../bots/scraper.js')
3 | const Images = require('../api/images.model.js')
4 |
5 | const betterTags = tags => (
6 | tags
7 | .map(e => e.split(',').join(''))
8 | .map(e => e.split('.').join(''))
9 | .map(e => e.charAt(0).toUpperCase() + e.slice(1))
10 | .filter(e => e.length > 1)
11 | .filter(e => e !== 'Or' && e !== 'And' && e !== 'By' && e !== 'The' && e !== 'In' && e !== 'Of')
12 | .filter(e => e !== 'Not' && e !== 'To' && e !== 'On' && e !== 'At' && e !== 'His' && e !== 'Her')
13 | )
14 |
15 | const persistImages = images => (
16 | images.forEach((item) => {
17 | new Images({
18 | url: item.url,
19 | title: item.title,
20 | source: item.source,
21 | tags: betterTags(item.tags),
22 | }).save()
23 | })
24 | )
25 |
26 | const scrapeImages = async (website) => {
27 | const scraper = new Scraper()
28 | try {
29 | await scraper.start()
30 | await scraper.api(website)
31 | await scraper.api.flow()
32 | const result = await scraper.api.export()
33 | persistImages(result)
34 | } catch(e) {
35 | console.error(e)
36 | } finally {
37 | await scraper.stop()
38 | }
39 | }
40 |
41 | module.exports = async () => {
42 | // await scrapeImages('pexels')
43 | // await scrapeImages('negative')
44 | // await scrapeImages('kaboom')
45 | // // scrapeImages('unsplash') TOFIX
46 |
47 | // // Automatically post on social medias TOFIX
48 | // // const image = database.get('images').sample().value()
49 | // // await require('../scrapers/twitter.js')(image)
50 |
51 | // // Reboot to cut any possible memory leaks
52 | // // Not working on heroku
53 | // // reboot((err, stderr, stdout) => {
54 | // // if(!err && !stderr) console.log(stdout)
55 | // // })
56 | }
57 |
--------------------------------------------------------------------------------
/src/client/containers/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { connect } from 'react-redux'
3 | import { reachBottom } from '../reducer'
4 | import Notification from '../components/Notification'
5 | import Display from '../components/Display'
6 | import ScrollUp from '../components/ScrollUp'
7 | import Loading from '../components/Loading'
8 | import Background from '../components/Background'
9 | import Header from './Header'
10 | import Gallery from './Gallery'
11 |
12 | const Home = ({ bottomReached = false, reachBottom = () => {} }) => {
13 |
14 | const onScroll = () => {
15 | const { body, documentElement } = document
16 | const windowHeight = 'innerHeight' in window ? window.innerHeight : documentElement.offsetHeight
17 | const windowBottom = windowHeight + window.pageYOffset + 300
18 | const docHeight = Math.max(
19 | body.scrollHeight, body.offsetHeight,
20 | documentElement.clientHeight, documentElement.scrollHeight, documentElement.offsetHeight,
21 | )
22 | if (windowBottom >= docHeight && !bottomReached) {
23 | reachBottom(true)
24 | } else if(bottomReached) {
25 | reachBottom(false)
26 | }
27 | }
28 |
29 | useEffect(() => {
30 | window.addEventListener('scroll', onScroll)
31 | return () => window.removeEventListener('scroll', onScroll)
32 | }, [bottomReached])
33 |
34 | return (
35 |
36 |
37 | {/* Background */}
38 |
39 |
40 | {/* Head */}
41 |
42 |
43 |
44 | {/* Body */}
45 |
46 |
47 |
48 |
49 | {/* Notif's snackbar */}
50 |
51 |
52 | )
53 | }
54 |
55 | const mapState = state => ({
56 | bottomReached: state.bottomReached,
57 | })
58 |
59 | const mapDispatch = dispatch => ({
60 | reachBottom: reached => dispatch(reachBottom(reached)),
61 | })
62 |
63 | export default connect(mapState, mapDispatch)(Home)
64 |
--------------------------------------------------------------------------------
/src/client/reducer.js:
--------------------------------------------------------------------------------
1 | const Actions = {
2 | MAKE_SEARCH: 'MAKE_SEARCH',
3 | UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION',
4 | REACH_BOTTOM: 'REACH_BOTTOM',
5 | IS_LOADING: 'IS_LOADING',
6 | CLEAN_IMAGES: 'CLEAN_IMAGES',
7 | ADD_IMAGE: 'ADD_IMAGE',
8 | UPDATE_DISPLAY: 'UPDATE_DISPLAY',
9 | }
10 |
11 | export const makeSearch = search => ({
12 | type: Actions.MAKE_SEARCH,
13 | payload: { search },
14 | })
15 |
16 | export const updateNotification = notification => ({
17 | type: Actions.UPDATE_NOTIFICATION,
18 | payload: { notification },
19 | })
20 |
21 | export const reachBottom = reached => ({
22 | type: Actions.REACH_BOTTOM,
23 | payload: { reached },
24 | })
25 |
26 | export const isLoading = loading => ({
27 | type: Actions.IS_LOADING,
28 | payload: { loading },
29 | })
30 |
31 | export const cleanImages = () => ({
32 | type: Actions.CLEAN_IMAGES,
33 | payload: { images: [] },
34 | })
35 |
36 | export const addImage = image => ({
37 | type: Actions.ADD_IMAGE,
38 | payload: { image },
39 | })
40 |
41 | export const updateDisplay = image => ({
42 | type: Actions.UPDATE_DISPLAY,
43 | payload: { image },
44 | })
45 |
46 | const defaultState = {
47 | search: '',
48 | notification: '',
49 | bottomReached: false,
50 | loading: false,
51 | images: [],
52 | display: {},
53 | }
54 |
55 | export default function reducer(prevState, action) {
56 | const state = prevState || defaultState
57 |
58 | switch (action.type) {
59 | case Actions.MAKE_SEARCH:
60 | return { ...state, search: action.payload.search }
61 | case Actions.UPDATE_NOTIFICATION:
62 | return { ...state, notification: action.payload.notification }
63 | case Actions.REACH_BOTTOM:
64 | return { ...state, bottomReached: action.payload.reached }
65 | case Actions.IS_LOADING:
66 | return { ...state, loading: action.payload.loading }
67 | case Actions.CLEAN_IMAGES:
68 | return { ...state, images: action.payload.images }
69 | case Actions.ADD_IMAGE:
70 | return { ...state, images: [...state.images, action.payload.image] }
71 | case Actions.UPDATE_DISPLAY:
72 | return { ...state, display: action.payload.image }
73 | default:
74 | return state
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/server/bots/negative.js:
--------------------------------------------------------------------------------
1 | const randomWord = require('random-words')
2 |
3 | // TODO scrape all pages
4 |
5 | class Negative {
6 |
7 | constructor(core) {
8 | this.core = core
9 | this.items = []
10 | this.data = []
11 | }
12 |
13 | async flow() {
14 | console.log('Negative Scraping (NS) started')
15 | await this.goto()
16 | await this.scroll()
17 | await this.collect()
18 | await this.browse()
19 | console.log('Negative Scraping (NS) ended')
20 | }
21 |
22 | async goto() {
23 | const word = randomWord()
24 | const search = `https://negativespace.co/?s=${word}`
25 | console.log(`NS : go to ${search}`)
26 | await this.core.goto(search)
27 | }
28 |
29 | async scroll() {
30 | const scroll = { height: 0, continue: false }
31 | console.log('NS : scrolling down')
32 | do {
33 | scroll.height = await this.core.evaluate('document.body.scrollHeight')
34 | await this.core.evaluate('window.scrollTo(0, document.body.scrollHeight)')
35 | await this.core.waitFor(3 * 1000)
36 | scroll.continue = await this.core.evaluate('document.body.scrollHeight') > scroll.height
37 | } while(scroll.continue)
38 | }
39 |
40 | async collect() {
41 | const length = (await this.core.$x('//*[@id="content"]/article')).length
42 | console.log('NS : collecting items')
43 | for (let image = 1; image < length + 1; image++) {
44 | const [item] = await this.core.$x(`//*[@id="content"]/article[${image}]/div[1]/a/img`)
45 | if (item) this.items.push(item)
46 | }
47 | }
48 |
49 | async browse() {
50 | console.log('NS : scraping items')
51 | for (let index = 0; index < this.items.length; index++) {
52 | await this.scrape(this.items[index])
53 | }
54 | }
55 |
56 | async scrape(item) {
57 | let title = await this.core.evaluate(e => e.getAttribute('title'), item)
58 | let url = await this.core.evaluate(e => e.getAttribute('src'), item)
59 |
60 | const tags = title.split(' ')
61 |
62 | this.data.push({ source: 'negative', tags, title, url })
63 | }
64 |
65 | async export() {
66 | console.log(`NS : exporting ${this.data.length} scraped photos`)
67 | return this.data
68 | }
69 |
70 | }
71 |
72 | module.exports = Negative
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const glob = require('glob')
5 | const HtmlWebpackPlugin = require('html-webpack-plugin')
6 | const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin')
7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
8 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
9 | const PurgeCssPlugin = require('purgecss-webpack-plugin')
10 | const CopyWebpackPlugin = require('copy-webpack-plugin')
11 | // const ESLintPlugin = require('eslint-webpack-plugin')
12 |
13 | module.exports = (env, argv) => ({
14 | entry: './src/client/index.js',
15 | output: {
16 | path: path.resolve(__dirname, './dist'),
17 | filename: 'main.[contenthash].js',
18 | clean: true,
19 | },
20 | target: ['web', 'es5'],
21 | module: {
22 | rules: [
23 | {
24 | test: /\.(js|jsx)$/,
25 | exclude: /node_modules/,
26 | use: 'babel-loader',
27 | },
28 | {
29 | test: /\.css$/,
30 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
31 | },
32 | {
33 | test: /\.(png|woff|woff2|eot|ttf|svg)$/,
34 | type: 'asset/resource',
35 | },
36 | ],
37 | },
38 | devtool: argv.mode === 'production' ? false : 'inline-source-map',
39 | devServer: {
40 | port: 3000,
41 | proxy: { '/api': 'http://localhost:8080' },
42 | hot: true,
43 | },
44 | plugins: [
45 | new HtmlWebpackPlugin({
46 | filename: path.resolve(__dirname, './dist')+'/index.html',
47 | template: './src/client/index.template.html',
48 | alwaysWriteToDisk: true,
49 | }),
50 | new HtmlWebpackHarddiskPlugin({
51 | outputPath: path.resolve(__dirname, './dist'),
52 | }),
53 | new MiniCssExtractPlugin({
54 | filename: 'style.[contenthash].css',
55 | }),
56 | new CssMinimizerPlugin(),
57 | new PurgeCssPlugin({
58 | paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, { nodir: true }),
59 | defaultExtractor: content => content.match(/[\w-:/]+(? {} }) => {
7 |
8 | const applyTranslation = (lang) => {
9 | close()
10 | localStorage.setItem('motada_language', lang)
11 | location.reload()
12 | }
13 |
14 | const languages = Object.keys(translations).map(key => (
15 |
24 | ))
25 |
26 | return open ? (
27 |
28 |
29 |
30 |
35 |
36 |
37 |
38 |
39 |
42 |
43 |
44 | {languages.map((l, i) => i % 2 == 0 ? l : null)}
45 |
46 |
47 | {languages.map((l, i) => i % 2 != 0 ? l : null)}
48 |
49 |
50 |
51 |
52 |
53 |
54 | ) : null
55 | }
56 |
57 | export default Translate
58 |
--------------------------------------------------------------------------------
/src/client/components/Image.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { connect } from 'react-redux'
3 | import { updateDisplay } from '../reducer'
4 | import axios from 'axios'
5 | import Zoom from 'react-reveal/Zoom'
6 |
7 | const Image = ({ data = {}, updateDisplay = () => {} }) => {
8 |
9 | const [hover, setHover] = useState(false)
10 | const [loaded, setLoaded] = useState(false)
11 |
12 | const click = () => {
13 | updateDisplay(data)
14 | setHover(false)
15 | axios.patch(`/api/images/${data._id}`, { click: data.click + 1 })
16 | }
17 |
18 | // generate random views count
19 | const min = 0
20 | const max = 10 * (data.click + 1)
21 | let views = (Math.floor(Math.random() * (max - min + 1)) + min).toString()
22 | const thousands = views.substring(0, views.length-3)
23 | const hundreds = views.substring(views.length-3, views.length-2)
24 | views = views < 1000 ? views : `${thousands}.${hundreds} k`
25 |
26 | return (
27 | setHover(true)}
31 | onMouseLeave={() => setHover(false)}
32 | >
33 |
34 |
![]()
setLoaded(true)}
36 | className="block w-full rounded shadow transform hover:scale-125 transition ease-in-out"
37 | src={`${data.url}?w=700`}
38 | alt={data.title}
39 | />
40 |
41 | {loaded && (
42 | hover ? (
43 |
44 |
45 |
46 |
47 |
48 | ) : (
49 |
50 | {views}
51 |
52 |
53 | )
54 | )}
55 |
56 |
57 | )
58 | }
59 |
60 | const mapDispatch = dispatch => ({
61 | updateDisplay: image => dispatch(updateDisplay(image)),
62 | })
63 |
64 | export default connect(() => ({}), mapDispatch)(Image)
65 |
--------------------------------------------------------------------------------
/src/server/bots/kaboom.js:
--------------------------------------------------------------------------------
1 | const randomWord = require('random-words')
2 |
3 | // TODO scrape all pages
4 |
5 | class Kaboom {
6 |
7 | constructor(core) {
8 | this.core = core
9 | this.items = []
10 | this.data = []
11 | }
12 |
13 | async flow() {
14 | console.log('Kaboom Scraping (KS) started')
15 | await this.goto()
16 | await this.scroll()
17 | await this.collect()
18 | await this.browse()
19 | console.log('Kaboom Scraping (KS) ended')
20 | }
21 |
22 | async goto() {
23 | const word = randomWord()
24 | const search = `https://kaboompics.com/gallery?search=${word}`
25 | console.log(`KS : go to ${search}`)
26 | await this.core.goto(search)
27 | }
28 |
29 | async scroll() {
30 | const scroll = { height: 0, continue: false }
31 | console.log('KS : scrolling down')
32 | do {
33 | scroll.height = await this.core.evaluate('document.body.scrollHeight')
34 | await this.core.evaluate('window.scrollTo(0, document.body.scrollHeight)')
35 | await this.core.waitFor(3 * 1000)
36 | scroll.continue = await this.core.evaluate('document.body.scrollHeight') > scroll.height
37 | } while(scroll.continue)
38 | }
39 |
40 | async collect() {
41 | const length = (await this.core.$x('//*[@id="work-grid"]/li')).length
42 | console.log('KS : collecting items')
43 | for (let image = 1; image < length + 1; image++) {
44 | const [item] = await this.core.$x(`//*[@id="work-grid"]/li[${image}]/div[1]/a/img`)
45 | if (item) this.items.push(item)
46 | }
47 | }
48 |
49 | async browse() {
50 | console.log('KS : scraping items')
51 | for (let index = 0; index < this.items.length; index++) {
52 | await this.scrape(this.items[index])
53 | }
54 | }
55 |
56 | async scrape(item) {
57 | let title = await this.core.evaluate(e => e.getAttribute('alt'), item)
58 | let url = await this.core.evaluate(e => e.getAttribute('data-original'), item)
59 |
60 | if (!title || !url) return
61 | if (title.indexOf('Kaboompics - ') !== -1) title = title.slice(13)
62 | if (url.indexOf('https') === -1) url = `https://kaboompics.com${url}`
63 |
64 | const tags = title.split(' ')
65 |
66 | this.data.push({ source: 'kaboom', tags, title, url })
67 | }
68 |
69 | async export() {
70 | console.log(`KS : exporting ${this.data.length} scraped photos`)
71 | return this.data
72 | }
73 |
74 | }
75 |
76 | module.exports = Kaboom
--------------------------------------------------------------------------------
/src/server/bots/twitter.js:
--------------------------------------------------------------------------------
1 | const Twitter = require('twitter')
2 | const request = require('request')
3 |
4 | let client
5 |
6 | const initTwitter = () => new Promise((resolve, reject) => {
7 | try {
8 | const config = require('../../../config')
9 | client = new Twitter({
10 | consumer_key: config.twitter.consumer_key,
11 | consumer_secret: config.twitter.consumer_secret,
12 | access_token_key: config.twitter.access_token,
13 | access_token_secret: config.twitter.access_token_secret,
14 | })
15 | resolve()
16 | } catch(error) {
17 | reject('Can\'t initialize Twitter client')
18 | }
19 | })
20 |
21 | const encodeImage = (url) => new Promise((resolve, reject) => {
22 | request({ url, encoding: null }, (error, response, body) => {
23 | if(body && response.statusCode === 200) {
24 | resolve(body.toString('base64'))
25 | } else {
26 | reject('Can\'t encode image from url')
27 | }
28 | })
29 | })
30 |
31 | module.exports = async (image) => {
32 | console.log('Twitter bot ON')
33 | let content = `Thousands free and high-res images on motada.io\n${image.title}\n`
34 | image.tags.forEach(tag => content += `#${tag} `)
35 |
36 | try {
37 | // Initializations
38 | await initTwitter()
39 | console.log('Twitter client ready')
40 |
41 | // Find country's id
42 | const countryTrends = await client.get('trends/available', {})
43 | const countryId = countryTrends.find(e => e.name === 'United States').woeid
44 |
45 | // Pick a random top trend for this country
46 | const placeTrends = await client.get('trends/place', { id: countryId })
47 | const selectedTrend = placeTrends[0].trends[Math.floor(Math.random()*placeTrends[0].trends.length)].name
48 | console.log(`Trend selected : ${selectedTrend}`)
49 | content += selectedTrend
50 |
51 | // Encode Image from url
52 | const base64Image = await encodeImage(`${image.url}?w=700`)
53 | console.log(`Image encoded : ${image.url}`)
54 |
55 | // Upload and get media's id
56 | const uploadMedia = await client.post('media/upload', { media_data: base64Image })
57 | const mediaId = uploadMedia.media_id_string
58 | console.log('Image uploaded')
59 |
60 | // Post content with media
61 | await client.post('statuses/update', { status: content, media_ids: mediaId })
62 | console.log('Post uploaded')
63 | console.log('Twitter bot OFF : success')
64 | } catch(error) {
65 | console.log(`Twitter bot OFF : ${JSON.stringify(error)}`)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/server/bots/pexels.js:
--------------------------------------------------------------------------------
1 | const randomWord = require('random-words')
2 |
3 | class Pexels {
4 |
5 | constructor(core) {
6 | this.core = core
7 | this.items = []
8 | this.data = []
9 | }
10 |
11 | async flow() {
12 | console.log('Pexels Scraping (PS) started')
13 | await this.goto()
14 | await this.scroll()
15 | await this.collect()
16 | await this.browse()
17 | console.log('Pexels Scraping (PS) ended')
18 | }
19 |
20 | async goto() {
21 | const word = randomWord()
22 | const search = `https://www.pexels.com/search/${word}`
23 | console.log(`PS : go to ${search}`)
24 | await this.core.goto(search)
25 | }
26 |
27 | async scroll() {
28 | const scroll = { height: 0, continue: false }
29 | console.log('PS : scrolling down')
30 | do {
31 | scroll.height = await this.core.evaluate('document.body.scrollHeight')
32 | await this.core.evaluate('window.scrollTo(0, document.body.scrollHeight)')
33 | await this.core.waitFor(3 * 1000)
34 | scroll.continue = await this.core.evaluate('document.body.scrollHeight') > scroll.height
35 | } while(scroll.continue)
36 | }
37 |
38 | async collect() {
39 | const columns = [
40 | (await this.core.$x('/html/body/div[1]/div[3]/div[2]/div[1]/div')).length,
41 | (await this.core.$x('/html/body/div[1]/div[3]/div[2]/div[2]/div')).length,
42 | ].filter(e => e != 0)
43 | console.log('PS : collecting items')
44 | for (let column = 1; column < columns.length + 1; column++) {
45 | for (let image = 1; image < columns[column - 1]; image++) {
46 | const [item] = await this.core.$x(`/html/body/div[1]/div[3]/div[2]/div[${column}]/div[${image}]/article/a[1]/img`)
47 | if (item) this.items.push(item)
48 | }
49 | }
50 | }
51 |
52 | async browse() {
53 | console.log('PS : scraping items')
54 | for (let index = 0; index < this.items.length; index++) {
55 | await this.scrape(this.items[index])
56 | }
57 | }
58 |
59 | async scrape(item) {
60 | let title = await this.core.evaluate(e => e.getAttribute('alt'), item)
61 | if (title.indexOf('Free stock photo of ') !== -1) title = title.slice(20)
62 |
63 | let url = await this.core.evaluate(e => e.getAttribute('srcset'), item)
64 | if (url.includes('?')) url = url.split('?')[0]
65 |
66 | const tags = title.split(' ')
67 |
68 | this.data.push({ source: 'pexels', tags, title, url })
69 | }
70 |
71 | async export() {
72 | console.log(`PS : exporting ${this.data.length} scraped photos`)
73 | return this.data
74 | }
75 |
76 | }
77 |
78 | module.exports = Pexels
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "motada-photos-browser",
3 | "version": "1.3.0",
4 | "description": "Search over thousands free images",
5 | "main": "src/server/index.js",
6 | "scripts": {
7 | "start": "node src/server/index.js",
8 | "build": "webpack --progress --mode production",
9 | "client": "webpack serve --color --hot --stats --mode development --env development",
10 | "analyze": "webpack --progress --analyze --mode production",
11 | "server": "nodemon src/server/index.js",
12 | "dev": "concurrently \"npm run server\" \"npm run client\"",
13 | "lint": "eslint ./src"
14 | },
15 | "author": "Quentin Dutot",
16 | "license": "ISC",
17 | "dependencies": {
18 | "algoliasearch": "^4.10.5",
19 | "axios": "^0.21.4",
20 | "body-parser": "^1.19.0",
21 | "chromedriver": "^2.46.0",
22 | "dotenv": "^10.0.0",
23 | "express": "^4.17.1",
24 | "file-saver": "^2.0.5",
25 | "mongoose": "^5.12.3",
26 | "node-cron": "^3.0.0",
27 | "nodejs-system-reboot": "0.0.4",
28 | "puppeteer": "^1.17.0",
29 | "random-words": "^1.1.1",
30 | "react": "^17.0.2",
31 | "react-dom": "^17.0.2",
32 | "react-flag-kit": "^0.5.0",
33 | "react-ga": "^3.3.0",
34 | "react-i18nify": "^4.1.2",
35 | "react-masonry-component": "^6.3.0",
36 | "react-particle-animation": "^2.0.2",
37 | "react-redux": "^7.2.5",
38 | "react-reveal": "^1.2.2",
39 | "react-scroll-up": "^1.3.7",
40 | "redux": "^4.1.1",
41 | "request": "^2.88.2",
42 | "ssl-express-www": "^3.0.8",
43 | "twitter": "^1.7.1"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "^7.15.5",
47 | "@babel/eslint-parser": "^7.15.4",
48 | "@babel/plugin-proposal-class-properties": "^7.14.5",
49 | "@babel/plugin-transform-runtime": "^7.15.0",
50 | "@babel/preset-env": "^7.15.6",
51 | "@babel/preset-react": "^7.14.5",
52 | "@babel/runtime": "^7.15.4",
53 | "babel-loader": "^8.2.2",
54 | "concurrently": "^6.2.1",
55 | "copy-webpack-plugin": "^9.0.1",
56 | "css-loader": "^6.2.0",
57 | "css-minimizer-webpack-plugin": "^3.0.2",
58 | "eslint": "^7.32.0",
59 | "eslint-config-airbnb": "^18.2.1",
60 | "eslint-plugin-import": "^2.24.2",
61 | "eslint-plugin-jsx-a11y": "^6.4.1",
62 | "eslint-plugin-react": "^7.25.1",
63 | "eslint-webpack-plugin": "^3.0.1",
64 | "html-webpack-harddisk-plugin": "^2.0.0",
65 | "html-webpack-plugin": "^5.3.2",
66 | "mini-css-extract-plugin": "^2.3.0",
67 | "nodemon": "^2.0.12",
68 | "purgecss-webpack-plugin": "^4.0.3",
69 | "webpack": "^5.52.1",
70 | "webpack-bundle-analyzer": "^4.4.2",
71 | "webpack-cli": "^4.8.0",
72 | "webpack-dev-server": "^4.2.0"
73 | },
74 | "optionalDependencies": {
75 | "fsevents": "2.3.2"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/server/bots/unsplash.js:
--------------------------------------------------------------------------------
1 | const randomWord = require('random-words')
2 |
3 | class Unsplash {
4 |
5 | constructor(core) {
6 | this.core = core
7 | this.items = []
8 | this.data = []
9 | }
10 |
11 | async flow() {
12 | console.log('Unsplash Scraping (US) started')
13 | await this.goto()
14 | await this.scroll()
15 | await this.collect()
16 | await this.browse()
17 | console.log('Unsplash Scraping (US) ended')
18 | }
19 |
20 | async goto() {
21 | const word = randomWord()
22 | const search = `https://unsplash.com/search/photos/${word}`
23 | console.log(`US : go to ${search}`)
24 | await this.core.goto(search)
25 | }
26 |
27 | async scroll() {
28 | const scroll = { height: 0, continue: false }
29 | console.log('US : scrolling down')
30 | do {
31 | scroll.height = await this.core.evaluate('document.body.scrollHeight')
32 | await this.core.evaluate('window.scrollTo(0, document.body.scrollHeight)')
33 | await this.core.waitFor(5 * 1000)
34 | scroll.continue = await this.core.evaluate('document.body.scrollHeight') > scroll.height
35 | } while(scroll.continue)
36 | }
37 |
38 | async collect() {
39 | const columns = [
40 | (await this.core.$x('//*[@id="app"]/div/div[5]/div[2]/div/div[1]/div/div/div[1]/figure')).length,
41 | (await this.core.$x('//*[@id="app"]/div/div[5]/div[2]/div/div[1]/div/div/div[2]/figure')).length,
42 | (await this.core.$x('//*[@id="app"]/div/div[5]/div[2]/div/div[1]/div/div/div[3]/figure')).length,
43 | ].filter(e => e != 0)
44 | console.log('US : collecting items')
45 | console.log(columns)
46 | for (let column = 1; column < columns.length + 1; column++) {
47 | for (let image = 1; image < columns[column - 1]; image++) {
48 | const [item] = await this.core.$x(`//*[@id="app"]/div/div[5]/div[2]/div/div[1]/div/div/div[${column}]/figure[${image}]/div/div/div[1]/div/a/div/img`)
49 | if (item) this.items.push(item)
50 | else console.log(item)
51 | }
52 | }
53 | }
54 |
55 | async browse() {
56 | console.log('US : scraping items')
57 | console.log(this.items.length)
58 | for (let index = 0; index < this.items.length; index++) {
59 | await this.scrape(this.items[index])
60 | }
61 | }
62 |
63 | async scrape(item) {
64 | let title = await this.core.evaluate(e => e.getAttribute('alt'), item)
65 | let url = await this.core.evaluate(e => e.getAttribute('srcset'), item)
66 | if (url.includes('?')) url = url.split('?')[0]
67 |
68 | const tags = title.split(' ')
69 |
70 | this.data.push({ source: 'unsplash', tags, title, url })
71 | }
72 |
73 | async export() {
74 | console.log(`US : exporting ${this.data.length} scraped photos`)
75 | return this.data
76 | }
77 |
78 | }
79 |
80 | module.exports = Unsplash
--------------------------------------------------------------------------------
/src/client/containers/Header.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { translate } from 'react-i18nify'
3 | import { FlagIcon } from 'react-flag-kit'
4 | import Description from '../components/Description'
5 | import Search from '../components/Search'
6 | import Translate from '../components/Translate'
7 | import axios from 'axios'
8 |
9 | const Header = () => {
10 |
11 | const [count, setCount] = useState(0)
12 | const [dialog, setDialog] = useState(false)
13 |
14 | useEffect(() => {
15 | axios('/api/images?count').then(response => setCount(response.data.count))
16 | }, [])
17 |
18 | const openGithubProjet = () => {
19 | window.open('https://github.com/QuentinDutot/motada-photos-browser', '_blank')
20 | }
21 |
22 | return (
23 |
24 |
25 |
26 | {/* Over the search area */}
27 |
28 |
29 | {count !== 0 ? translate('header.title', { count: `${Math.round(count/1000)}k` }) : translate('header.default_title')}
30 |
31 |
32 |
setDialog(true)}
37 | />
38 |
46 |
47 |
48 |
49 | {/* The search area */}
50 |
51 |
52 | {/* Under the search area */}
53 |
54 |
55 | {/* The translation popup */}
56 |
setDialog(false)} />
57 |
58 |
59 | )
60 | }
61 |
62 | export default Header
63 |
--------------------------------------------------------------------------------
/src/client/containers/Gallery.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { translate } from 'react-i18nify'
3 | import { connect } from 'react-redux'
4 | import { updateNotification, isLoading, cleanImages, addImage } from '../reducer'
5 | import Image from '../components/Image'
6 | import Masonry from 'react-masonry-component'
7 | import axios from 'axios'
8 | import NoData from '../components/NoData'
9 |
10 | let timeouts = []
11 |
12 | const Gallery = ({
13 | search = '',
14 | bottomReached = false,
15 | loading = false,
16 | images = [],
17 | isLoading = () => {},
18 | updateNotification = () => {},
19 | cleanImages = () => {},
20 | addImage = () => {},
21 | }) => {
22 |
23 | const [limit, setLimit] = useState(10)
24 |
25 | const saveImages = (_search, _images) => {
26 | timeouts = []
27 | for (let i = 0; i < _images.length; i++) {
28 | const tmp = setTimeout(() => {
29 | if (!images.find(image => image._id === _images[i]._id) && _search === search) {
30 | addImage(_images[i])
31 | }
32 | }, i * 500)
33 | timeouts.push(tmp)
34 | }
35 | }
36 |
37 | const request = (url, type) => {
38 | axios(url)
39 | .then((response) => {
40 | // console.log(response.data)
41 | if (response.data[type] && response.data[type].length > 0) {
42 | saveImages(search, response.data[type])
43 | } else {
44 | updateNotification(translate('errors.no_results'))
45 | }
46 | })
47 | .catch((error) => {
48 | // console.log(error)
49 | updateNotification(translate('errors.unknow'))
50 | })
51 | .then(() => isLoading(false))
52 | }
53 |
54 | const loadSearch = (_search) => {
55 | isLoading(true)
56 | request(`/api/images?search=${_search}`, 'search')
57 | }
58 |
59 | const loadRandom = (_limit) => {
60 | isLoading(true)
61 | request(`/api/images?random=${_limit}`, 'random')
62 | }
63 |
64 | useEffect(() => {
65 | cleanImages()
66 | timeouts.forEach(timeout => clearTimeout(timeout))
67 | timeouts = []
68 | if (search) loadSearch(search)
69 | else loadRandom(50)
70 | }, [search])
71 |
72 | useEffect(() => {
73 | if (!bottomReached || loading) return
74 | if (limit < images.length) setLimit(limit + 10)
75 | else if (!search) loadRandom(50)
76 | }, [bottomReached, loading])
77 |
78 | return (
79 | images.length ? (
80 |
81 | {images.slice(0, limit).map(image => (
82 |
83 | ))}
84 |
85 | ) : (
86 |
87 | )
88 | )
89 | }
90 |
91 | const mapState = state => ({
92 | search: state.search,
93 | images: state.images,
94 | loading: state.loading,
95 | bottomReached: state.bottomReached,
96 | })
97 |
98 | const mapDispatch = dispatch => ({
99 | isLoading: loading => dispatch(isLoading(loading)),
100 | updateNotification: notification => dispatch(updateNotification(notification)),
101 | cleanImages: () => dispatch(cleanImages()),
102 | addImage: image => dispatch(addImage(image)),
103 | })
104 |
105 | export default connect(mapState, mapDispatch)(Gallery)
106 |
--------------------------------------------------------------------------------
/src/client/components/Display.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { translate } from 'react-i18nify'
3 | import { connect } from 'react-redux'
4 | import { makeSearch, updateDisplay } from '../reducer'
5 | import FileSaver from 'file-saver'
6 | import axios from 'axios'
7 | import Slide from 'react-reveal/Slide'
8 | import Fade from 'react-reveal/Fade'
9 |
10 | const Display = ({
11 | display = {},
12 | makeSearch = () => {},
13 | updateDisplay = () => {},
14 | }) => {
15 |
16 | const [loaded, setLoaded] = useState(false)
17 | const [admin, setAdmin] = useState(false)
18 |
19 | const saveImage = async (url) => {
20 | let filename = url.substring(url.lastIndexOf('/')+1)
21 | if (filename.indexOf('.jpeg') === -1
22 | && filename.indexOf('.jpg') === -1
23 | && filename.indexOf('.png') === -1) {
24 | filename += '.jpg'
25 | }
26 | FileSaver.saveAs(url, filename)
27 | }
28 |
29 | const exploreTag = (tag) => {
30 | updateDisplay({})
31 | makeSearch(tag)
32 | }
33 |
34 | const onKeyDown = (event) => {
35 | if (!admin) {
36 | setAdmin(event.keyCode == 46)
37 | }
38 | }
39 |
40 | useEffect(() => {
41 | window.addEventListener('keydown', onKeyDown)
42 | return () => window.removeEventListener('keydown', onKeyDown)
43 | }, [admin])
44 |
45 | useEffect(() => {
46 | document.body.style.overflow = display && display.url ? 'hidden' : null
47 | }, [JSON.stringify(display)])
48 |
49 | return display && display.url ? (
50 |
51 | updateDisplay({})}>
52 |
53 |
54 | event.stopPropagation()}
57 | >
58 |
{display.title}
59 |
60 |
68 |
76 |
77 |
78 |
79 |
80 | {!loaded && (
81 |
82 |
83 |
84 | )}
85 |
86 |
87 |
event.stopPropagation()}
94 | onLoad={() => setLoaded(true)}
95 | />
96 |
97 |
98 |
99 | event.stopPropagation()}
102 | >
103 |
104 | {`${translate('tooltips.keywords')}: `}
105 | {display.tags.map(tag =>
106 |
113 | )}
114 |
115 | {admin && (
116 |
{
120 | if (event.target.value === process.env.ADMIN_KEY) {
121 | axios.delete(`/api/images/${display._id}`).then(({ data }) => {
122 | if (data) {
123 | setAdmin(false)
124 | location.reload()
125 | }
126 | })
127 | }
128 | }}
129 | />
130 | )}
131 |
132 |
133 |
134 |
135 |
136 | ) : null
137 | }
138 |
139 | const mapState = state => ({
140 | display: state.display,
141 | })
142 |
143 | const mapDispatch = dispatch => ({
144 | makeSearch: search => dispatch(makeSearch(search)),
145 | updateDisplay: image => dispatch(updateDisplay(image)),
146 | })
147 |
148 | export default connect(mapState, mapDispatch)(Display)
149 |
--------------------------------------------------------------------------------
/src/client/polyfills/promise.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
3 | typeof define === 'function' && define.amd ? define(factory) :
4 | (factory());
5 | }(this, (function () { 'use strict';
6 |
7 | /**
8 | * @this {Promise}
9 | */
10 | function finallyConstructor(callback) {
11 | var constructor = this.constructor;
12 | return this.then(
13 | function(value) {
14 | // @ts-ignore
15 | return constructor.resolve(callback()).then(function() {
16 | return value;
17 | });
18 | },
19 | function(reason) {
20 | // @ts-ignore
21 | return constructor.resolve(callback()).then(function() {
22 | // @ts-ignore
23 | return constructor.reject(reason);
24 | });
25 | }
26 | );
27 | }
28 |
29 | function allSettled(arr) {
30 | var P = this;
31 | return new P(function(resolve, reject) {
32 | if (!(arr && typeof arr.length !== 'undefined')) {
33 | return reject(
34 | new TypeError(
35 | typeof arr +
36 | ' ' +
37 | arr +
38 | ' is not iterable(cannot read property Symbol(Symbol.iterator))'
39 | )
40 | );
41 | }
42 | var args = Array.prototype.slice.call(arr);
43 | if (args.length === 0) return resolve([]);
44 | var remaining = args.length;
45 |
46 | function res(i, val) {
47 | if (val && (typeof val === 'object' || typeof val === 'function')) {
48 | var then = val.then;
49 | if (typeof then === 'function') {
50 | then.call(
51 | val,
52 | function(val) {
53 | res(i, val);
54 | },
55 | function(e) {
56 | args[i] = { status: 'rejected', reason: e };
57 | if (--remaining === 0) {
58 | resolve(args);
59 | }
60 | }
61 | );
62 | return;
63 | }
64 | }
65 | args[i] = { status: 'fulfilled', value: val };
66 | if (--remaining === 0) {
67 | resolve(args);
68 | }
69 | }
70 |
71 | for (var i = 0; i < args.length; i++) {
72 | res(i, args[i]);
73 | }
74 | });
75 | }
76 |
77 | // Store setTimeout reference so promise-polyfill will be unaffected by
78 | // other code modifying setTimeout (like sinon.useFakeTimers())
79 | var setTimeoutFunc = setTimeout;
80 |
81 | function isArray(x) {
82 | return Boolean(x && typeof x.length !== 'undefined');
83 | }
84 |
85 | function noop() {}
86 |
87 | // Polyfill for Function.prototype.bind
88 | function bind(fn, thisArg) {
89 | return function() {
90 | fn.apply(thisArg, arguments);
91 | };
92 | }
93 |
94 | /**
95 | * @constructor
96 | * @param {Function} fn
97 | */
98 | function Promise(fn) {
99 | if (!(this instanceof Promise))
100 | throw new TypeError('Promises must be constructed via new');
101 | if (typeof fn !== 'function') throw new TypeError('not a function');
102 | /** @type {!number} */
103 | this._state = 0;
104 | /** @type {!boolean} */
105 | this._handled = false;
106 | /** @type {Promise|undefined} */
107 | this._value = undefined;
108 | /** @type {!Array} */
109 | this._deferreds = [];
110 |
111 | doResolve(fn, this);
112 | }
113 |
114 | function handle(self, deferred) {
115 | while (self._state === 3) {
116 | self = self._value;
117 | }
118 | if (self._state === 0) {
119 | self._deferreds.push(deferred);
120 | return;
121 | }
122 | self._handled = true;
123 | Promise._immediateFn(function() {
124 | var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
125 | if (cb === null) {
126 | (self._state === 1 ? resolve : reject)(deferred.promise, self._value);
127 | return;
128 | }
129 | var ret;
130 | try {
131 | ret = cb(self._value);
132 | } catch (e) {
133 | reject(deferred.promise, e);
134 | return;
135 | }
136 | resolve(deferred.promise, ret);
137 | });
138 | }
139 |
140 | function resolve(self, newValue) {
141 | try {
142 | // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
143 | if (newValue === self)
144 | throw new TypeError('A promise cannot be resolved with itself.');
145 | if (
146 | newValue &&
147 | (typeof newValue === 'object' || typeof newValue === 'function')
148 | ) {
149 | var then = newValue.then;
150 | if (newValue instanceof Promise) {
151 | self._state = 3;
152 | self._value = newValue;
153 | finale(self);
154 | return;
155 | } else if (typeof then === 'function') {
156 | doResolve(bind(then, newValue), self);
157 | return;
158 | }
159 | }
160 | self._state = 1;
161 | self._value = newValue;
162 | finale(self);
163 | } catch (e) {
164 | reject(self, e);
165 | }
166 | }
167 |
168 | function reject(self, newValue) {
169 | self._state = 2;
170 | self._value = newValue;
171 | finale(self);
172 | }
173 |
174 | function finale(self) {
175 | if (self._state === 2 && self._deferreds.length === 0) {
176 | Promise._immediateFn(function() {
177 | if (!self._handled) {
178 | Promise._unhandledRejectionFn(self._value);
179 | }
180 | });
181 | }
182 |
183 | for (var i = 0, len = self._deferreds.length; i < len; i++) {
184 | handle(self, self._deferreds[i]);
185 | }
186 | self._deferreds = null;
187 | }
188 |
189 | /**
190 | * @constructor
191 | */
192 | function Handler(onFulfilled, onRejected, promise) {
193 | this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
194 | this.onRejected = typeof onRejected === 'function' ? onRejected : null;
195 | this.promise = promise;
196 | }
197 |
198 | /**
199 | * Take a potentially misbehaving resolver function and make sure
200 | * onFulfilled and onRejected are only called once.
201 | *
202 | * Makes no guarantees about asynchrony.
203 | */
204 | function doResolve(fn, self) {
205 | var done = false;
206 | try {
207 | fn(
208 | function(value) {
209 | if (done) return;
210 | done = true;
211 | resolve(self, value);
212 | },
213 | function(reason) {
214 | if (done) return;
215 | done = true;
216 | reject(self, reason);
217 | }
218 | );
219 | } catch (ex) {
220 | if (done) return;
221 | done = true;
222 | reject(self, ex);
223 | }
224 | }
225 |
226 | Promise.prototype['catch'] = function(onRejected) {
227 | return this.then(null, onRejected);
228 | };
229 |
230 | Promise.prototype.then = function(onFulfilled, onRejected) {
231 | // @ts-ignore
232 | var prom = new this.constructor(noop);
233 |
234 | handle(this, new Handler(onFulfilled, onRejected, prom));
235 | return prom;
236 | };
237 |
238 | Promise.prototype['finally'] = finallyConstructor;
239 |
240 | Promise.all = function(arr) {
241 | return new Promise(function(resolve, reject) {
242 | if (!isArray(arr)) {
243 | return reject(new TypeError('Promise.all accepts an array'));
244 | }
245 |
246 | var args = Array.prototype.slice.call(arr);
247 | if (args.length === 0) return resolve([]);
248 | var remaining = args.length;
249 |
250 | function res(i, val) {
251 | try {
252 | if (val && (typeof val === 'object' || typeof val === 'function')) {
253 | var then = val.then;
254 | if (typeof then === 'function') {
255 | then.call(
256 | val,
257 | function(val) {
258 | res(i, val);
259 | },
260 | reject
261 | );
262 | return;
263 | }
264 | }
265 | args[i] = val;
266 | if (--remaining === 0) {
267 | resolve(args);
268 | }
269 | } catch (ex) {
270 | reject(ex);
271 | }
272 | }
273 |
274 | for (var i = 0; i < args.length; i++) {
275 | res(i, args[i]);
276 | }
277 | });
278 | };
279 |
280 | Promise.allSettled = allSettled;
281 |
282 | Promise.resolve = function(value) {
283 | if (value && typeof value === 'object' && value.constructor === Promise) {
284 | return value;
285 | }
286 |
287 | return new Promise(function(resolve) {
288 | resolve(value);
289 | });
290 | };
291 |
292 | Promise.reject = function(value) {
293 | return new Promise(function(resolve, reject) {
294 | reject(value);
295 | });
296 | };
297 |
298 | Promise.race = function(arr) {
299 | return new Promise(function(resolve, reject) {
300 | if (!isArray(arr)) {
301 | return reject(new TypeError('Promise.race accepts an array'));
302 | }
303 |
304 | for (var i = 0, len = arr.length; i < len; i++) {
305 | Promise.resolve(arr[i]).then(resolve, reject);
306 | }
307 | });
308 | };
309 |
310 | // Use polyfill for setImmediate for performance gains
311 | Promise._immediateFn =
312 | // @ts-ignore
313 | (typeof setImmediate === 'function' &&
314 | function(fn) {
315 | // @ts-ignore
316 | setImmediate(fn);
317 | }) ||
318 | function(fn) {
319 | setTimeoutFunc(fn, 0);
320 | };
321 |
322 | Promise._unhandledRejectionFn = function _unhandledRejectionFn(err) {
323 | if (typeof console !== 'undefined' && console) {
324 | console.warn('Possible Unhandled Promise Rejection:', err); // eslint-disable-line no-console
325 | }
326 | };
327 |
328 | /** @suppress {undefinedVars} */
329 | var globalNS = (function() {
330 | // the only reliable means to get the global object is
331 | // `Function('return this')()`
332 | // However, this causes CSP violations in Chrome apps.
333 | if (typeof self !== 'undefined') {
334 | return self;
335 | }
336 | if (typeof window !== 'undefined') {
337 | return window;
338 | }
339 | if (typeof global !== 'undefined') {
340 | return global;
341 | }
342 | throw new Error('unable to locate global object');
343 | })();
344 |
345 | // Expose the polyfill if Promise is undefined or set to a
346 | // non-function value. The latter can be due to a named HTMLElement
347 | // being exposed by browsers for legacy reasons.
348 | // https://github.com/taylorhakes/promise-polyfill/issues/114
349 | if (typeof globalNS['Promise'] !== 'function') {
350 | globalNS['Promise'] = Promise;
351 | } else if (!globalNS.Promise.prototype['finally']) {
352 | globalNS.Promise.prototype['finally'] = finallyConstructor;
353 | } else if (!globalNS.Promise.allSettled) {
354 | globalNS.Promise.allSettled = allSettled;
355 | }
356 |
357 | })));
358 |
--------------------------------------------------------------------------------
/src/assets/styles/font-awesome.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
4 | */
5 | @font-face {
6 | font-family: "FontAwesome";
7 | src: url("../fonts/fontawesome-webfont.eot?v=4.7.0");
8 | src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0")
9 | format("embedded-opentype"),
10 | url("../fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"),
11 | url("../fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"),
12 | url("../fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"),
13 | url("../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular")
14 | format("svg");
15 | font-weight: normal;
16 | font-style: normal;
17 | }
18 | .fa {
19 | display: inline-block;
20 | font: normal normal normal 14px/1 FontAwesome;
21 | font-size: inherit;
22 | text-rendering: auto;
23 | -webkit-font-smoothing: antialiased;
24 | -moz-osx-font-smoothing: grayscale;
25 | }
26 | .fa-lg {
27 | font-size: 1.33333333em;
28 | line-height: 0.75em;
29 | vertical-align: -15%;
30 | }
31 | .fa-2x {
32 | font-size: 2em;
33 | }
34 | .fa-3x {
35 | font-size: 3em;
36 | }
37 | .fa-4x {
38 | font-size: 4em;
39 | }
40 | .fa-5x {
41 | font-size: 5em;
42 | }
43 | .fa-fw {
44 | width: 1.28571429em;
45 | text-align: center;
46 | }
47 | .fa-ul {
48 | padding-left: 0;
49 | margin-left: 2.14285714em;
50 | list-style-type: none;
51 | }
52 | .fa-ul > li {
53 | position: relative;
54 | }
55 | .fa-li {
56 | position: absolute;
57 | left: -2.14285714em;
58 | width: 2.14285714em;
59 | top: 0.14285714em;
60 | text-align: center;
61 | }
62 | .fa-li.fa-lg {
63 | left: -1.85714286em;
64 | }
65 | .fa-border {
66 | padding: 0.2em 0.25em 0.15em;
67 | border: solid 0.08em #eee;
68 | border-radius: 0.1em;
69 | }
70 | .fa-pull-left {
71 | float: left;
72 | }
73 | .fa-pull-right {
74 | float: right;
75 | }
76 | .fa.fa-pull-left {
77 | margin-right: 0.3em;
78 | }
79 | .fa.fa-pull-right {
80 | margin-left: 0.3em;
81 | }
82 | .pull-right {
83 | float: right;
84 | }
85 | .pull-left {
86 | float: left;
87 | }
88 | .fa.pull-left {
89 | margin-right: 0.3em;
90 | }
91 | .fa.pull-right {
92 | margin-left: 0.3em;
93 | }
94 | .fa-spin {
95 | -webkit-animation: fa-spin 2s infinite linear;
96 | animation: fa-spin 2s infinite linear;
97 | }
98 | .fa-pulse {
99 | -webkit-animation: fa-spin 1s infinite steps(8);
100 | animation: fa-spin 1s infinite steps(8);
101 | }
102 | @-webkit-keyframes fa-spin {
103 | 0% {
104 | -webkit-transform: rotate(0deg);
105 | transform: rotate(0deg);
106 | }
107 | 100% {
108 | -webkit-transform: rotate(359deg);
109 | transform: rotate(359deg);
110 | }
111 | }
112 | @keyframes fa-spin {
113 | 0% {
114 | -webkit-transform: rotate(0deg);
115 | transform: rotate(0deg);
116 | }
117 | 100% {
118 | -webkit-transform: rotate(359deg);
119 | transform: rotate(359deg);
120 | }
121 | }
122 | .fa-rotate-90 {
123 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
124 | -webkit-transform: rotate(90deg);
125 | -ms-transform: rotate(90deg);
126 | transform: rotate(90deg);
127 | }
128 | .fa-rotate-180 {
129 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
130 | -webkit-transform: rotate(180deg);
131 | -ms-transform: rotate(180deg);
132 | transform: rotate(180deg);
133 | }
134 | .fa-rotate-270 {
135 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
136 | -webkit-transform: rotate(270deg);
137 | -ms-transform: rotate(270deg);
138 | transform: rotate(270deg);
139 | }
140 | .fa-flip-horizontal {
141 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
142 | -webkit-transform: scale(-1, 1);
143 | -ms-transform: scale(-1, 1);
144 | transform: scale(-1, 1);
145 | }
146 | .fa-flip-vertical {
147 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
148 | -webkit-transform: scale(1, -1);
149 | -ms-transform: scale(1, -1);
150 | transform: scale(1, -1);
151 | }
152 | :root .fa-rotate-90,
153 | :root .fa-rotate-180,
154 | :root .fa-rotate-270,
155 | :root .fa-flip-horizontal,
156 | :root .fa-flip-vertical {
157 | filter: none;
158 | }
159 | .fa-stack {
160 | position: relative;
161 | display: inline-block;
162 | width: 2em;
163 | height: 2em;
164 | line-height: 2em;
165 | vertical-align: middle;
166 | }
167 | .fa-stack-1x,
168 | .fa-stack-2x {
169 | position: absolute;
170 | left: 0;
171 | width: 100%;
172 | text-align: center;
173 | }
174 | .fa-stack-1x {
175 | line-height: inherit;
176 | }
177 | .fa-stack-2x {
178 | font-size: 2em;
179 | }
180 | .fa-inverse {
181 | color: #fff;
182 | }
183 | .fa-glass:before {
184 | content: "\f000";
185 | }
186 | .fa-music:before {
187 | content: "\f001";
188 | }
189 | .fa-search:before {
190 | content: "\f002";
191 | }
192 | .fa-envelope-o:before {
193 | content: "\f003";
194 | }
195 | .fa-heart:before {
196 | content: "\f004";
197 | }
198 | .fa-star:before {
199 | content: "\f005";
200 | }
201 | .fa-star-o:before {
202 | content: "\f006";
203 | }
204 | .fa-user:before {
205 | content: "\f007";
206 | }
207 | .fa-film:before {
208 | content: "\f008";
209 | }
210 | .fa-th-large:before {
211 | content: "\f009";
212 | }
213 | .fa-th:before {
214 | content: "\f00a";
215 | }
216 | .fa-th-list:before {
217 | content: "\f00b";
218 | }
219 | .fa-check:before {
220 | content: "\f00c";
221 | }
222 | .fa-remove:before,
223 | .fa-close:before,
224 | .fa-times:before {
225 | content: "\f00d";
226 | }
227 | .fa-search-plus:before {
228 | content: "\f00e";
229 | }
230 | .fa-search-minus:before {
231 | content: "\f010";
232 | }
233 | .fa-power-off:before {
234 | content: "\f011";
235 | }
236 | .fa-signal:before {
237 | content: "\f012";
238 | }
239 | .fa-gear:before,
240 | .fa-cog:before {
241 | content: "\f013";
242 | }
243 | .fa-trash-o:before {
244 | content: "\f014";
245 | }
246 | .fa-home:before {
247 | content: "\f015";
248 | }
249 | .fa-file-o:before {
250 | content: "\f016";
251 | }
252 | .fa-clock-o:before {
253 | content: "\f017";
254 | }
255 | .fa-road:before {
256 | content: "\f018";
257 | }
258 | .fa-download:before {
259 | content: "\f019";
260 | }
261 | .fa-arrow-circle-o-down:before {
262 | content: "\f01a";
263 | }
264 | .fa-arrow-circle-o-up:before {
265 | content: "\f01b";
266 | }
267 | .fa-inbox:before {
268 | content: "\f01c";
269 | }
270 | .fa-play-circle-o:before {
271 | content: "\f01d";
272 | }
273 | .fa-rotate-right:before,
274 | .fa-repeat:before {
275 | content: "\f01e";
276 | }
277 | .fa-refresh:before {
278 | content: "\f021";
279 | }
280 | .fa-list-alt:before {
281 | content: "\f022";
282 | }
283 | .fa-lock:before {
284 | content: "\f023";
285 | }
286 | .fa-flag:before {
287 | content: "\f024";
288 | }
289 | .fa-headphones:before {
290 | content: "\f025";
291 | }
292 | .fa-volume-off:before {
293 | content: "\f026";
294 | }
295 | .fa-volume-down:before {
296 | content: "\f027";
297 | }
298 | .fa-volume-up:before {
299 | content: "\f028";
300 | }
301 | .fa-qrcode:before {
302 | content: "\f029";
303 | }
304 | .fa-barcode:before {
305 | content: "\f02a";
306 | }
307 | .fa-tag:before {
308 | content: "\f02b";
309 | }
310 | .fa-tags:before {
311 | content: "\f02c";
312 | }
313 | .fa-book:before {
314 | content: "\f02d";
315 | }
316 | .fa-bookmark:before {
317 | content: "\f02e";
318 | }
319 | .fa-print:before {
320 | content: "\f02f";
321 | }
322 | .fa-camera:before {
323 | content: "\f030";
324 | }
325 | .fa-font:before {
326 | content: "\f031";
327 | }
328 | .fa-bold:before {
329 | content: "\f032";
330 | }
331 | .fa-italic:before {
332 | content: "\f033";
333 | }
334 | .fa-text-height:before {
335 | content: "\f034";
336 | }
337 | .fa-text-width:before {
338 | content: "\f035";
339 | }
340 | .fa-align-left:before {
341 | content: "\f036";
342 | }
343 | .fa-align-center:before {
344 | content: "\f037";
345 | }
346 | .fa-align-right:before {
347 | content: "\f038";
348 | }
349 | .fa-align-justify:before {
350 | content: "\f039";
351 | }
352 | .fa-list:before {
353 | content: "\f03a";
354 | }
355 | .fa-dedent:before,
356 | .fa-outdent:before {
357 | content: "\f03b";
358 | }
359 | .fa-indent:before {
360 | content: "\f03c";
361 | }
362 | .fa-video-camera:before {
363 | content: "\f03d";
364 | }
365 | .fa-photo:before,
366 | .fa-image:before,
367 | .fa-picture-o:before {
368 | content: "\f03e";
369 | }
370 | .fa-pencil:before {
371 | content: "\f040";
372 | }
373 | .fa-map-marker:before {
374 | content: "\f041";
375 | }
376 | .fa-adjust:before {
377 | content: "\f042";
378 | }
379 | .fa-tint:before {
380 | content: "\f043";
381 | }
382 | .fa-edit:before,
383 | .fa-pencil-square-o:before {
384 | content: "\f044";
385 | }
386 | .fa-share-square-o:before {
387 | content: "\f045";
388 | }
389 | .fa-check-square-o:before {
390 | content: "\f046";
391 | }
392 | .fa-arrows:before {
393 | content: "\f047";
394 | }
395 | .fa-step-backward:before {
396 | content: "\f048";
397 | }
398 | .fa-fast-backward:before {
399 | content: "\f049";
400 | }
401 | .fa-backward:before {
402 | content: "\f04a";
403 | }
404 | .fa-play:before {
405 | content: "\f04b";
406 | }
407 | .fa-pause:before {
408 | content: "\f04c";
409 | }
410 | .fa-stop:before {
411 | content: "\f04d";
412 | }
413 | .fa-forward:before {
414 | content: "\f04e";
415 | }
416 | .fa-fast-forward:before {
417 | content: "\f050";
418 | }
419 | .fa-step-forward:before {
420 | content: "\f051";
421 | }
422 | .fa-eject:before {
423 | content: "\f052";
424 | }
425 | .fa-chevron-left:before {
426 | content: "\f053";
427 | }
428 | .fa-chevron-right:before {
429 | content: "\f054";
430 | }
431 | .fa-plus-circle:before {
432 | content: "\f055";
433 | }
434 | .fa-minus-circle:before {
435 | content: "\f056";
436 | }
437 | .fa-times-circle:before {
438 | content: "\f057";
439 | }
440 | .fa-check-circle:before {
441 | content: "\f058";
442 | }
443 | .fa-question-circle:before {
444 | content: "\f059";
445 | }
446 | .fa-info-circle:before {
447 | content: "\f05a";
448 | }
449 | .fa-crosshairs:before {
450 | content: "\f05b";
451 | }
452 | .fa-times-circle-o:before {
453 | content: "\f05c";
454 | }
455 | .fa-check-circle-o:before {
456 | content: "\f05d";
457 | }
458 | .fa-ban:before {
459 | content: "\f05e";
460 | }
461 | .fa-arrow-left:before {
462 | content: "\f060";
463 | }
464 | .fa-arrow-right:before {
465 | content: "\f061";
466 | }
467 | .fa-arrow-up:before {
468 | content: "\f062";
469 | }
470 | .fa-arrow-down:before {
471 | content: "\f063";
472 | }
473 | .fa-mail-forward:before,
474 | .fa-share:before {
475 | content: "\f064";
476 | }
477 | .fa-expand:before {
478 | content: "\f065";
479 | }
480 | .fa-compress:before {
481 | content: "\f066";
482 | }
483 | .fa-plus:before {
484 | content: "\f067";
485 | }
486 | .fa-minus:before {
487 | content: "\f068";
488 | }
489 | .fa-asterisk:before {
490 | content: "\f069";
491 | }
492 | .fa-exclamation-circle:before {
493 | content: "\f06a";
494 | }
495 | .fa-gift:before {
496 | content: "\f06b";
497 | }
498 | .fa-leaf:before {
499 | content: "\f06c";
500 | }
501 | .fa-fire:before {
502 | content: "\f06d";
503 | }
504 | .fa-eye:before {
505 | content: "\f06e";
506 | }
507 | .fa-eye-slash:before {
508 | content: "\f070";
509 | }
510 | .fa-warning:before,
511 | .fa-exclamation-triangle:before {
512 | content: "\f071";
513 | }
514 | .fa-plane:before {
515 | content: "\f072";
516 | }
517 | .fa-calendar:before {
518 | content: "\f073";
519 | }
520 | .fa-random:before {
521 | content: "\f074";
522 | }
523 | .fa-comment:before {
524 | content: "\f075";
525 | }
526 | .fa-magnet:before {
527 | content: "\f076";
528 | }
529 | .fa-chevron-up:before {
530 | content: "\f077";
531 | }
532 | .fa-chevron-down:before {
533 | content: "\f078";
534 | }
535 | .fa-retweet:before {
536 | content: "\f079";
537 | }
538 | .fa-shopping-cart:before {
539 | content: "\f07a";
540 | }
541 | .fa-folder:before {
542 | content: "\f07b";
543 | }
544 | .fa-folder-open:before {
545 | content: "\f07c";
546 | }
547 | .fa-arrows-v:before {
548 | content: "\f07d";
549 | }
550 | .fa-arrows-h:before {
551 | content: "\f07e";
552 | }
553 | .fa-bar-chart-o:before,
554 | .fa-bar-chart:before {
555 | content: "\f080";
556 | }
557 | .fa-twitter-square:before {
558 | content: "\f081";
559 | }
560 | .fa-facebook-square:before {
561 | content: "\f082";
562 | }
563 | .fa-camera-retro:before {
564 | content: "\f083";
565 | }
566 | .fa-key:before {
567 | content: "\f084";
568 | }
569 | .fa-gears:before,
570 | .fa-cogs:before {
571 | content: "\f085";
572 | }
573 | .fa-comments:before {
574 | content: "\f086";
575 | }
576 | .fa-thumbs-o-up:before {
577 | content: "\f087";
578 | }
579 | .fa-thumbs-o-down:before {
580 | content: "\f088";
581 | }
582 | .fa-star-half:before {
583 | content: "\f089";
584 | }
585 | .fa-heart-o:before {
586 | content: "\f08a";
587 | }
588 | .fa-sign-out:before {
589 | content: "\f08b";
590 | }
591 | .fa-linkedin-square:before {
592 | content: "\f08c";
593 | }
594 | .fa-thumb-tack:before {
595 | content: "\f08d";
596 | }
597 | .fa-external-link:before {
598 | content: "\f08e";
599 | }
600 | .fa-sign-in:before {
601 | content: "\f090";
602 | }
603 | .fa-trophy:before {
604 | content: "\f091";
605 | }
606 | .fa-github-square:before {
607 | content: "\f092";
608 | }
609 | .fa-upload:before {
610 | content: "\f093";
611 | }
612 | .fa-lemon-o:before {
613 | content: "\f094";
614 | }
615 | .fa-phone:before {
616 | content: "\f095";
617 | }
618 | .fa-square-o:before {
619 | content: "\f096";
620 | }
621 | .fa-bookmark-o:before {
622 | content: "\f097";
623 | }
624 | .fa-phone-square:before {
625 | content: "\f098";
626 | }
627 | .fa-twitter:before {
628 | content: "\f099";
629 | }
630 | .fa-facebook-f:before,
631 | .fa-facebook:before {
632 | content: "\f09a";
633 | }
634 | .fa-github:before {
635 | content: "\f09b";
636 | }
637 | .fa-unlock:before {
638 | content: "\f09c";
639 | }
640 | .fa-credit-card:before {
641 | content: "\f09d";
642 | }
643 | .fa-feed:before,
644 | .fa-rss:before {
645 | content: "\f09e";
646 | }
647 | .fa-hdd-o:before {
648 | content: "\f0a0";
649 | }
650 | .fa-bullhorn:before {
651 | content: "\f0a1";
652 | }
653 | .fa-bell:before {
654 | content: "\f0f3";
655 | }
656 | .fa-certificate:before {
657 | content: "\f0a3";
658 | }
659 | .fa-hand-o-right:before {
660 | content: "\f0a4";
661 | }
662 | .fa-hand-o-left:before {
663 | content: "\f0a5";
664 | }
665 | .fa-hand-o-up:before {
666 | content: "\f0a6";
667 | }
668 | .fa-hand-o-down:before {
669 | content: "\f0a7";
670 | }
671 | .fa-arrow-circle-left:before {
672 | content: "\f0a8";
673 | }
674 | .fa-arrow-circle-right:before {
675 | content: "\f0a9";
676 | }
677 | .fa-arrow-circle-up:before {
678 | content: "\f0aa";
679 | }
680 | .fa-arrow-circle-down:before {
681 | content: "\f0ab";
682 | }
683 | .fa-globe:before {
684 | content: "\f0ac";
685 | }
686 | .fa-wrench:before {
687 | content: "\f0ad";
688 | }
689 | .fa-tasks:before {
690 | content: "\f0ae";
691 | }
692 | .fa-filter:before {
693 | content: "\f0b0";
694 | }
695 | .fa-briefcase:before {
696 | content: "\f0b1";
697 | }
698 | .fa-arrows-alt:before {
699 | content: "\f0b2";
700 | }
701 | .fa-group:before,
702 | .fa-users:before {
703 | content: "\f0c0";
704 | }
705 | .fa-chain:before,
706 | .fa-link:before {
707 | content: "\f0c1";
708 | }
709 | .fa-cloud:before {
710 | content: "\f0c2";
711 | }
712 | .fa-flask:before {
713 | content: "\f0c3";
714 | }
715 | .fa-cut:before,
716 | .fa-scissors:before {
717 | content: "\f0c4";
718 | }
719 | .fa-copy:before,
720 | .fa-files-o:before {
721 | content: "\f0c5";
722 | }
723 | .fa-paperclip:before {
724 | content: "\f0c6";
725 | }
726 | .fa-save:before,
727 | .fa-floppy-o:before {
728 | content: "\f0c7";
729 | }
730 | .fa-square:before {
731 | content: "\f0c8";
732 | }
733 | .fa-navicon:before,
734 | .fa-reorder:before,
735 | .fa-bars:before {
736 | content: "\f0c9";
737 | }
738 | .fa-list-ul:before {
739 | content: "\f0ca";
740 | }
741 | .fa-list-ol:before {
742 | content: "\f0cb";
743 | }
744 | .fa-strikethrough:before {
745 | content: "\f0cc";
746 | }
747 | .fa-underline:before {
748 | content: "\f0cd";
749 | }
750 | .fa-table:before {
751 | content: "\f0ce";
752 | }
753 | .fa-magic:before {
754 | content: "\f0d0";
755 | }
756 | .fa-truck:before {
757 | content: "\f0d1";
758 | }
759 | .fa-pinterest:before {
760 | content: "\f0d2";
761 | }
762 | .fa-pinterest-square:before {
763 | content: "\f0d3";
764 | }
765 | .fa-google-plus-square:before {
766 | content: "\f0d4";
767 | }
768 | .fa-google-plus:before {
769 | content: "\f0d5";
770 | }
771 | .fa-money:before {
772 | content: "\f0d6";
773 | }
774 | .fa-caret-down:before {
775 | content: "\f0d7";
776 | }
777 | .fa-caret-up:before {
778 | content: "\f0d8";
779 | }
780 | .fa-caret-left:before {
781 | content: "\f0d9";
782 | }
783 | .fa-caret-right:before {
784 | content: "\f0da";
785 | }
786 | .fa-columns:before {
787 | content: "\f0db";
788 | }
789 | .fa-unsorted:before,
790 | .fa-sort:before {
791 | content: "\f0dc";
792 | }
793 | .fa-sort-down:before,
794 | .fa-sort-desc:before {
795 | content: "\f0dd";
796 | }
797 | .fa-sort-up:before,
798 | .fa-sort-asc:before {
799 | content: "\f0de";
800 | }
801 | .fa-envelope:before {
802 | content: "\f0e0";
803 | }
804 | .fa-linkedin:before {
805 | content: "\f0e1";
806 | }
807 | .fa-rotate-left:before,
808 | .fa-undo:before {
809 | content: "\f0e2";
810 | }
811 | .fa-legal:before,
812 | .fa-gavel:before {
813 | content: "\f0e3";
814 | }
815 | .fa-dashboard:before,
816 | .fa-tachometer:before {
817 | content: "\f0e4";
818 | }
819 | .fa-comment-o:before {
820 | content: "\f0e5";
821 | }
822 | .fa-comments-o:before {
823 | content: "\f0e6";
824 | }
825 | .fa-flash:before,
826 | .fa-bolt:before {
827 | content: "\f0e7";
828 | }
829 | .fa-sitemap:before {
830 | content: "\f0e8";
831 | }
832 | .fa-umbrella:before {
833 | content: "\f0e9";
834 | }
835 | .fa-paste:before,
836 | .fa-clipboard:before {
837 | content: "\f0ea";
838 | }
839 | .fa-lightbulb-o:before {
840 | content: "\f0eb";
841 | }
842 | .fa-exchange:before {
843 | content: "\f0ec";
844 | }
845 | .fa-cloud-download:before {
846 | content: "\f0ed";
847 | }
848 | .fa-cloud-upload:before {
849 | content: "\f0ee";
850 | }
851 | .fa-user-md:before {
852 | content: "\f0f0";
853 | }
854 | .fa-stethoscope:before {
855 | content: "\f0f1";
856 | }
857 | .fa-suitcase:before {
858 | content: "\f0f2";
859 | }
860 | .fa-bell-o:before {
861 | content: "\f0a2";
862 | }
863 | .fa-coffee:before {
864 | content: "\f0f4";
865 | }
866 | .fa-cutlery:before {
867 | content: "\f0f5";
868 | }
869 | .fa-file-text-o:before {
870 | content: "\f0f6";
871 | }
872 | .fa-building-o:before {
873 | content: "\f0f7";
874 | }
875 | .fa-hospital-o:before {
876 | content: "\f0f8";
877 | }
878 | .fa-ambulance:before {
879 | content: "\f0f9";
880 | }
881 | .fa-medkit:before {
882 | content: "\f0fa";
883 | }
884 | .fa-fighter-jet:before {
885 | content: "\f0fb";
886 | }
887 | .fa-beer:before {
888 | content: "\f0fc";
889 | }
890 | .fa-h-square:before {
891 | content: "\f0fd";
892 | }
893 | .fa-plus-square:before {
894 | content: "\f0fe";
895 | }
896 | .fa-angle-double-left:before {
897 | content: "\f100";
898 | }
899 | .fa-angle-double-right:before {
900 | content: "\f101";
901 | }
902 | .fa-angle-double-up:before {
903 | content: "\f102";
904 | }
905 | .fa-angle-double-down:before {
906 | content: "\f103";
907 | }
908 | .fa-angle-left:before {
909 | content: "\f104";
910 | }
911 | .fa-angle-right:before {
912 | content: "\f105";
913 | }
914 | .fa-angle-up:before {
915 | content: "\f106";
916 | }
917 | .fa-angle-down:before {
918 | content: "\f107";
919 | }
920 | .fa-desktop:before {
921 | content: "\f108";
922 | }
923 | .fa-laptop:before {
924 | content: "\f109";
925 | }
926 | .fa-tablet:before {
927 | content: "\f10a";
928 | }
929 | .fa-mobile-phone:before,
930 | .fa-mobile:before {
931 | content: "\f10b";
932 | }
933 | .fa-circle-o:before {
934 | content: "\f10c";
935 | }
936 | .fa-quote-left:before {
937 | content: "\f10d";
938 | }
939 | .fa-quote-right:before {
940 | content: "\f10e";
941 | }
942 | .fa-spinner:before {
943 | content: "\f110";
944 | }
945 | .fa-circle:before {
946 | content: "\f111";
947 | }
948 | .fa-mail-reply:before,
949 | .fa-reply:before {
950 | content: "\f112";
951 | }
952 | .fa-github-alt:before {
953 | content: "\f113";
954 | }
955 | .fa-folder-o:before {
956 | content: "\f114";
957 | }
958 | .fa-folder-open-o:before {
959 | content: "\f115";
960 | }
961 | .fa-smile-o:before {
962 | content: "\f118";
963 | }
964 | .fa-frown-o:before {
965 | content: "\f119";
966 | }
967 | .fa-meh-o:before {
968 | content: "\f11a";
969 | }
970 | .fa-gamepad:before {
971 | content: "\f11b";
972 | }
973 | .fa-keyboard-o:before {
974 | content: "\f11c";
975 | }
976 | .fa-flag-o:before {
977 | content: "\f11d";
978 | }
979 | .fa-flag-checkered:before {
980 | content: "\f11e";
981 | }
982 | .fa-terminal:before {
983 | content: "\f120";
984 | }
985 | .fa-code:before {
986 | content: "\f121";
987 | }
988 | .fa-mail-reply-all:before,
989 | .fa-reply-all:before {
990 | content: "\f122";
991 | }
992 | .fa-star-half-empty:before,
993 | .fa-star-half-full:before,
994 | .fa-star-half-o:before {
995 | content: "\f123";
996 | }
997 | .fa-location-arrow:before {
998 | content: "\f124";
999 | }
1000 | .fa-crop:before {
1001 | content: "\f125";
1002 | }
1003 | .fa-code-fork:before {
1004 | content: "\f126";
1005 | }
1006 | .fa-unlink:before,
1007 | .fa-chain-broken:before {
1008 | content: "\f127";
1009 | }
1010 | .fa-question:before {
1011 | content: "\f128";
1012 | }
1013 | .fa-info:before {
1014 | content: "\f129";
1015 | }
1016 | .fa-exclamation:before {
1017 | content: "\f12a";
1018 | }
1019 | .fa-superscript:before {
1020 | content: "\f12b";
1021 | }
1022 | .fa-subscript:before {
1023 | content: "\f12c";
1024 | }
1025 | .fa-eraser:before {
1026 | content: "\f12d";
1027 | }
1028 | .fa-puzzle-piece:before {
1029 | content: "\f12e";
1030 | }
1031 | .fa-microphone:before {
1032 | content: "\f130";
1033 | }
1034 | .fa-microphone-slash:before {
1035 | content: "\f131";
1036 | }
1037 | .fa-shield:before {
1038 | content: "\f132";
1039 | }
1040 | .fa-calendar-o:before {
1041 | content: "\f133";
1042 | }
1043 | .fa-fire-extinguisher:before {
1044 | content: "\f134";
1045 | }
1046 | .fa-rocket:before {
1047 | content: "\f135";
1048 | }
1049 | .fa-maxcdn:before {
1050 | content: "\f136";
1051 | }
1052 | .fa-chevron-circle-left:before {
1053 | content: "\f137";
1054 | }
1055 | .fa-chevron-circle-right:before {
1056 | content: "\f138";
1057 | }
1058 | .fa-chevron-circle-up:before {
1059 | content: "\f139";
1060 | }
1061 | .fa-chevron-circle-down:before {
1062 | content: "\f13a";
1063 | }
1064 | .fa-html5:before {
1065 | content: "\f13b";
1066 | }
1067 | .fa-css3:before {
1068 | content: "\f13c";
1069 | }
1070 | .fa-anchor:before {
1071 | content: "\f13d";
1072 | }
1073 | .fa-unlock-alt:before {
1074 | content: "\f13e";
1075 | }
1076 | .fa-bullseye:before {
1077 | content: "\f140";
1078 | }
1079 | .fa-ellipsis-h:before {
1080 | content: "\f141";
1081 | }
1082 | .fa-ellipsis-v:before {
1083 | content: "\f142";
1084 | }
1085 | .fa-rss-square:before {
1086 | content: "\f143";
1087 | }
1088 | .fa-play-circle:before {
1089 | content: "\f144";
1090 | }
1091 | .fa-ticket:before {
1092 | content: "\f145";
1093 | }
1094 | .fa-minus-square:before {
1095 | content: "\f146";
1096 | }
1097 | .fa-minus-square-o:before {
1098 | content: "\f147";
1099 | }
1100 | .fa-level-up:before {
1101 | content: "\f148";
1102 | }
1103 | .fa-level-down:before {
1104 | content: "\f149";
1105 | }
1106 | .fa-check-square:before {
1107 | content: "\f14a";
1108 | }
1109 | .fa-pencil-square:before {
1110 | content: "\f14b";
1111 | }
1112 | .fa-external-link-square:before {
1113 | content: "\f14c";
1114 | }
1115 | .fa-share-square:before {
1116 | content: "\f14d";
1117 | }
1118 | .fa-compass:before {
1119 | content: "\f14e";
1120 | }
1121 | .fa-toggle-down:before,
1122 | .fa-caret-square-o-down:before {
1123 | content: "\f150";
1124 | }
1125 | .fa-toggle-up:before,
1126 | .fa-caret-square-o-up:before {
1127 | content: "\f151";
1128 | }
1129 | .fa-toggle-right:before,
1130 | .fa-caret-square-o-right:before {
1131 | content: "\f152";
1132 | }
1133 | .fa-euro:before,
1134 | .fa-eur:before {
1135 | content: "\f153";
1136 | }
1137 | .fa-gbp:before {
1138 | content: "\f154";
1139 | }
1140 | .fa-dollar:before,
1141 | .fa-usd:before {
1142 | content: "\f155";
1143 | }
1144 | .fa-rupee:before,
1145 | .fa-inr:before {
1146 | content: "\f156";
1147 | }
1148 | .fa-cny:before,
1149 | .fa-rmb:before,
1150 | .fa-yen:before,
1151 | .fa-jpy:before {
1152 | content: "\f157";
1153 | }
1154 | .fa-ruble:before,
1155 | .fa-rouble:before,
1156 | .fa-rub:before {
1157 | content: "\f158";
1158 | }
1159 | .fa-won:before,
1160 | .fa-krw:before {
1161 | content: "\f159";
1162 | }
1163 | .fa-bitcoin:before,
1164 | .fa-btc:before {
1165 | content: "\f15a";
1166 | }
1167 | .fa-file:before {
1168 | content: "\f15b";
1169 | }
1170 | .fa-file-text:before {
1171 | content: "\f15c";
1172 | }
1173 | .fa-sort-alpha-asc:before {
1174 | content: "\f15d";
1175 | }
1176 | .fa-sort-alpha-desc:before {
1177 | content: "\f15e";
1178 | }
1179 | .fa-sort-amount-asc:before {
1180 | content: "\f160";
1181 | }
1182 | .fa-sort-amount-desc:before {
1183 | content: "\f161";
1184 | }
1185 | .fa-sort-numeric-asc:before {
1186 | content: "\f162";
1187 | }
1188 | .fa-sort-numeric-desc:before {
1189 | content: "\f163";
1190 | }
1191 | .fa-thumbs-up:before {
1192 | content: "\f164";
1193 | }
1194 | .fa-thumbs-down:before {
1195 | content: "\f165";
1196 | }
1197 | .fa-youtube-square:before {
1198 | content: "\f166";
1199 | }
1200 | .fa-youtube:before {
1201 | content: "\f167";
1202 | }
1203 | .fa-xing:before {
1204 | content: "\f168";
1205 | }
1206 | .fa-xing-square:before {
1207 | content: "\f169";
1208 | }
1209 | .fa-youtube-play:before {
1210 | content: "\f16a";
1211 | }
1212 | .fa-dropbox:before {
1213 | content: "\f16b";
1214 | }
1215 | .fa-stack-overflow:before {
1216 | content: "\f16c";
1217 | }
1218 | .fa-instagram:before {
1219 | content: "\f16d";
1220 | }
1221 | .fa-flickr:before {
1222 | content: "\f16e";
1223 | }
1224 | .fa-adn:before {
1225 | content: "\f170";
1226 | }
1227 | .fa-bitbucket:before {
1228 | content: "\f171";
1229 | }
1230 | .fa-bitbucket-square:before {
1231 | content: "\f172";
1232 | }
1233 | .fa-tumblr:before {
1234 | content: "\f173";
1235 | }
1236 | .fa-tumblr-square:before {
1237 | content: "\f174";
1238 | }
1239 | .fa-long-arrow-down:before {
1240 | content: "\f175";
1241 | }
1242 | .fa-long-arrow-up:before {
1243 | content: "\f176";
1244 | }
1245 | .fa-long-arrow-left:before {
1246 | content: "\f177";
1247 | }
1248 | .fa-long-arrow-right:before {
1249 | content: "\f178";
1250 | }
1251 | .fa-apple:before {
1252 | content: "\f179";
1253 | }
1254 | .fa-windows:before {
1255 | content: "\f17a";
1256 | }
1257 | .fa-android:before {
1258 | content: "\f17b";
1259 | }
1260 | .fa-linux:before {
1261 | content: "\f17c";
1262 | }
1263 | .fa-dribbble:before {
1264 | content: "\f17d";
1265 | }
1266 | .fa-skype:before {
1267 | content: "\f17e";
1268 | }
1269 | .fa-foursquare:before {
1270 | content: "\f180";
1271 | }
1272 | .fa-trello:before {
1273 | content: "\f181";
1274 | }
1275 | .fa-female:before {
1276 | content: "\f182";
1277 | }
1278 | .fa-male:before {
1279 | content: "\f183";
1280 | }
1281 | .fa-gittip:before,
1282 | .fa-gratipay:before {
1283 | content: "\f184";
1284 | }
1285 | .fa-sun-o:before {
1286 | content: "\f185";
1287 | }
1288 | .fa-moon-o:before {
1289 | content: "\f186";
1290 | }
1291 | .fa-archive:before {
1292 | content: "\f187";
1293 | }
1294 | .fa-bug:before {
1295 | content: "\f188";
1296 | }
1297 | .fa-vk:before {
1298 | content: "\f189";
1299 | }
1300 | .fa-weibo:before {
1301 | content: "\f18a";
1302 | }
1303 | .fa-renren:before {
1304 | content: "\f18b";
1305 | }
1306 | .fa-pagelines:before {
1307 | content: "\f18c";
1308 | }
1309 | .fa-stack-exchange:before {
1310 | content: "\f18d";
1311 | }
1312 | .fa-arrow-circle-o-right:before {
1313 | content: "\f18e";
1314 | }
1315 | .fa-arrow-circle-o-left:before {
1316 | content: "\f190";
1317 | }
1318 | .fa-toggle-left:before,
1319 | .fa-caret-square-o-left:before {
1320 | content: "\f191";
1321 | }
1322 | .fa-dot-circle-o:before {
1323 | content: "\f192";
1324 | }
1325 | .fa-wheelchair:before {
1326 | content: "\f193";
1327 | }
1328 | .fa-vimeo-square:before {
1329 | content: "\f194";
1330 | }
1331 | .fa-turkish-lira:before,
1332 | .fa-try:before {
1333 | content: "\f195";
1334 | }
1335 | .fa-plus-square-o:before {
1336 | content: "\f196";
1337 | }
1338 | .fa-space-shuttle:before {
1339 | content: "\f197";
1340 | }
1341 | .fa-slack:before {
1342 | content: "\f198";
1343 | }
1344 | .fa-envelope-square:before {
1345 | content: "\f199";
1346 | }
1347 | .fa-wordpress:before {
1348 | content: "\f19a";
1349 | }
1350 | .fa-openid:before {
1351 | content: "\f19b";
1352 | }
1353 | .fa-institution:before,
1354 | .fa-bank:before,
1355 | .fa-university:before {
1356 | content: "\f19c";
1357 | }
1358 | .fa-mortar-board:before,
1359 | .fa-graduation-cap:before {
1360 | content: "\f19d";
1361 | }
1362 | .fa-yahoo:before {
1363 | content: "\f19e";
1364 | }
1365 | .fa-google:before {
1366 | content: "\f1a0";
1367 | }
1368 | .fa-reddit:before {
1369 | content: "\f1a1";
1370 | }
1371 | .fa-reddit-square:before {
1372 | content: "\f1a2";
1373 | }
1374 | .fa-stumbleupon-circle:before {
1375 | content: "\f1a3";
1376 | }
1377 | .fa-stumbleupon:before {
1378 | content: "\f1a4";
1379 | }
1380 | .fa-delicious:before {
1381 | content: "\f1a5";
1382 | }
1383 | .fa-digg:before {
1384 | content: "\f1a6";
1385 | }
1386 | .fa-pied-piper-pp:before {
1387 | content: "\f1a7";
1388 | }
1389 | .fa-pied-piper-alt:before {
1390 | content: "\f1a8";
1391 | }
1392 | .fa-drupal:before {
1393 | content: "\f1a9";
1394 | }
1395 | .fa-joomla:before {
1396 | content: "\f1aa";
1397 | }
1398 | .fa-language:before {
1399 | content: "\f1ab";
1400 | }
1401 | .fa-fax:before {
1402 | content: "\f1ac";
1403 | }
1404 | .fa-building:before {
1405 | content: "\f1ad";
1406 | }
1407 | .fa-child:before {
1408 | content: "\f1ae";
1409 | }
1410 | .fa-paw:before {
1411 | content: "\f1b0";
1412 | }
1413 | .fa-spoon:before {
1414 | content: "\f1b1";
1415 | }
1416 | .fa-cube:before {
1417 | content: "\f1b2";
1418 | }
1419 | .fa-cubes:before {
1420 | content: "\f1b3";
1421 | }
1422 | .fa-behance:before {
1423 | content: "\f1b4";
1424 | }
1425 | .fa-behance-square:before {
1426 | content: "\f1b5";
1427 | }
1428 | .fa-steam:before {
1429 | content: "\f1b6";
1430 | }
1431 | .fa-steam-square:before {
1432 | content: "\f1b7";
1433 | }
1434 | .fa-recycle:before {
1435 | content: "\f1b8";
1436 | }
1437 | .fa-automobile:before,
1438 | .fa-car:before {
1439 | content: "\f1b9";
1440 | }
1441 | .fa-cab:before,
1442 | .fa-taxi:before {
1443 | content: "\f1ba";
1444 | }
1445 | .fa-tree:before {
1446 | content: "\f1bb";
1447 | }
1448 | .fa-spotify:before {
1449 | content: "\f1bc";
1450 | }
1451 | .fa-deviantart:before {
1452 | content: "\f1bd";
1453 | }
1454 | .fa-soundcloud:before {
1455 | content: "\f1be";
1456 | }
1457 | .fa-database:before {
1458 | content: "\f1c0";
1459 | }
1460 | .fa-file-pdf-o:before {
1461 | content: "\f1c1";
1462 | }
1463 | .fa-file-word-o:before {
1464 | content: "\f1c2";
1465 | }
1466 | .fa-file-excel-o:before {
1467 | content: "\f1c3";
1468 | }
1469 | .fa-file-powerpoint-o:before {
1470 | content: "\f1c4";
1471 | }
1472 | .fa-file-photo-o:before,
1473 | .fa-file-picture-o:before,
1474 | .fa-file-image-o:before {
1475 | content: "\f1c5";
1476 | }
1477 | .fa-file-zip-o:before,
1478 | .fa-file-archive-o:before {
1479 | content: "\f1c6";
1480 | }
1481 | .fa-file-sound-o:before,
1482 | .fa-file-audio-o:before {
1483 | content: "\f1c7";
1484 | }
1485 | .fa-file-movie-o:before,
1486 | .fa-file-video-o:before {
1487 | content: "\f1c8";
1488 | }
1489 | .fa-file-code-o:before {
1490 | content: "\f1c9";
1491 | }
1492 | .fa-vine:before {
1493 | content: "\f1ca";
1494 | }
1495 | .fa-codepen:before {
1496 | content: "\f1cb";
1497 | }
1498 | .fa-jsfiddle:before {
1499 | content: "\f1cc";
1500 | }
1501 | .fa-life-bouy:before,
1502 | .fa-life-buoy:before,
1503 | .fa-life-saver:before,
1504 | .fa-support:before,
1505 | .fa-life-ring:before {
1506 | content: "\f1cd";
1507 | }
1508 | .fa-circle-o-notch:before {
1509 | content: "\f1ce";
1510 | }
1511 | .fa-ra:before,
1512 | .fa-resistance:before,
1513 | .fa-rebel:before {
1514 | content: "\f1d0";
1515 | }
1516 | .fa-ge:before,
1517 | .fa-empire:before {
1518 | content: "\f1d1";
1519 | }
1520 | .fa-git-square:before {
1521 | content: "\f1d2";
1522 | }
1523 | .fa-git:before {
1524 | content: "\f1d3";
1525 | }
1526 | .fa-y-combinator-square:before,
1527 | .fa-yc-square:before,
1528 | .fa-hacker-news:before {
1529 | content: "\f1d4";
1530 | }
1531 | .fa-tencent-weibo:before {
1532 | content: "\f1d5";
1533 | }
1534 | .fa-qq:before {
1535 | content: "\f1d6";
1536 | }
1537 | .fa-wechat:before,
1538 | .fa-weixin:before {
1539 | content: "\f1d7";
1540 | }
1541 | .fa-send:before,
1542 | .fa-paper-plane:before {
1543 | content: "\f1d8";
1544 | }
1545 | .fa-send-o:before,
1546 | .fa-paper-plane-o:before {
1547 | content: "\f1d9";
1548 | }
1549 | .fa-history:before {
1550 | content: "\f1da";
1551 | }
1552 | .fa-circle-thin:before {
1553 | content: "\f1db";
1554 | }
1555 | .fa-header:before {
1556 | content: "\f1dc";
1557 | }
1558 | .fa-paragraph:before {
1559 | content: "\f1dd";
1560 | }
1561 | .fa-sliders:before {
1562 | content: "\f1de";
1563 | }
1564 | .fa-share-alt:before {
1565 | content: "\f1e0";
1566 | }
1567 | .fa-share-alt-square:before {
1568 | content: "\f1e1";
1569 | }
1570 | .fa-bomb:before {
1571 | content: "\f1e2";
1572 | }
1573 | .fa-soccer-ball-o:before,
1574 | .fa-futbol-o:before {
1575 | content: "\f1e3";
1576 | }
1577 | .fa-tty:before {
1578 | content: "\f1e4";
1579 | }
1580 | .fa-binoculars:before {
1581 | content: "\f1e5";
1582 | }
1583 | .fa-plug:before {
1584 | content: "\f1e6";
1585 | }
1586 | .fa-slideshare:before {
1587 | content: "\f1e7";
1588 | }
1589 | .fa-twitch:before {
1590 | content: "\f1e8";
1591 | }
1592 | .fa-yelp:before {
1593 | content: "\f1e9";
1594 | }
1595 | .fa-newspaper-o:before {
1596 | content: "\f1ea";
1597 | }
1598 | .fa-wifi:before {
1599 | content: "\f1eb";
1600 | }
1601 | .fa-calculator:before {
1602 | content: "\f1ec";
1603 | }
1604 | .fa-paypal:before {
1605 | content: "\f1ed";
1606 | }
1607 | .fa-google-wallet:before {
1608 | content: "\f1ee";
1609 | }
1610 | .fa-cc-visa:before {
1611 | content: "\f1f0";
1612 | }
1613 | .fa-cc-mastercard:before {
1614 | content: "\f1f1";
1615 | }
1616 | .fa-cc-discover:before {
1617 | content: "\f1f2";
1618 | }
1619 | .fa-cc-amex:before {
1620 | content: "\f1f3";
1621 | }
1622 | .fa-cc-paypal:before {
1623 | content: "\f1f4";
1624 | }
1625 | .fa-cc-stripe:before {
1626 | content: "\f1f5";
1627 | }
1628 | .fa-bell-slash:before {
1629 | content: "\f1f6";
1630 | }
1631 | .fa-bell-slash-o:before {
1632 | content: "\f1f7";
1633 | }
1634 | .fa-trash:before {
1635 | content: "\f1f8";
1636 | }
1637 | .fa-copyright:before {
1638 | content: "\f1f9";
1639 | }
1640 | .fa-at:before {
1641 | content: "\f1fa";
1642 | }
1643 | .fa-eyedropper:before {
1644 | content: "\f1fb";
1645 | }
1646 | .fa-paint-brush:before {
1647 | content: "\f1fc";
1648 | }
1649 | .fa-birthday-cake:before {
1650 | content: "\f1fd";
1651 | }
1652 | .fa-area-chart:before {
1653 | content: "\f1fe";
1654 | }
1655 | .fa-pie-chart:before {
1656 | content: "\f200";
1657 | }
1658 | .fa-line-chart:before {
1659 | content: "\f201";
1660 | }
1661 | .fa-lastfm:before {
1662 | content: "\f202";
1663 | }
1664 | .fa-lastfm-square:before {
1665 | content: "\f203";
1666 | }
1667 | .fa-toggle-off:before {
1668 | content: "\f204";
1669 | }
1670 | .fa-toggle-on:before {
1671 | content: "\f205";
1672 | }
1673 | .fa-bicycle:before {
1674 | content: "\f206";
1675 | }
1676 | .fa-bus:before {
1677 | content: "\f207";
1678 | }
1679 | .fa-ioxhost:before {
1680 | content: "\f208";
1681 | }
1682 | .fa-angellist:before {
1683 | content: "\f209";
1684 | }
1685 | .fa-cc:before {
1686 | content: "\f20a";
1687 | }
1688 | .fa-shekel:before,
1689 | .fa-sheqel:before,
1690 | .fa-ils:before {
1691 | content: "\f20b";
1692 | }
1693 | .fa-meanpath:before {
1694 | content: "\f20c";
1695 | }
1696 | .fa-buysellads:before {
1697 | content: "\f20d";
1698 | }
1699 | .fa-connectdevelop:before {
1700 | content: "\f20e";
1701 | }
1702 | .fa-dashcube:before {
1703 | content: "\f210";
1704 | }
1705 | .fa-forumbee:before {
1706 | content: "\f211";
1707 | }
1708 | .fa-leanpub:before {
1709 | content: "\f212";
1710 | }
1711 | .fa-sellsy:before {
1712 | content: "\f213";
1713 | }
1714 | .fa-shirtsinbulk:before {
1715 | content: "\f214";
1716 | }
1717 | .fa-simplybuilt:before {
1718 | content: "\f215";
1719 | }
1720 | .fa-skyatlas:before {
1721 | content: "\f216";
1722 | }
1723 | .fa-cart-plus:before {
1724 | content: "\f217";
1725 | }
1726 | .fa-cart-arrow-down:before {
1727 | content: "\f218";
1728 | }
1729 | .fa-diamond:before {
1730 | content: "\f219";
1731 | }
1732 | .fa-ship:before {
1733 | content: "\f21a";
1734 | }
1735 | .fa-user-secret:before {
1736 | content: "\f21b";
1737 | }
1738 | .fa-motorcycle:before {
1739 | content: "\f21c";
1740 | }
1741 | .fa-street-view:before {
1742 | content: "\f21d";
1743 | }
1744 | .fa-heartbeat:before {
1745 | content: "\f21e";
1746 | }
1747 | .fa-venus:before {
1748 | content: "\f221";
1749 | }
1750 | .fa-mars:before {
1751 | content: "\f222";
1752 | }
1753 | .fa-mercury:before {
1754 | content: "\f223";
1755 | }
1756 | .fa-intersex:before,
1757 | .fa-transgender:before {
1758 | content: "\f224";
1759 | }
1760 | .fa-transgender-alt:before {
1761 | content: "\f225";
1762 | }
1763 | .fa-venus-double:before {
1764 | content: "\f226";
1765 | }
1766 | .fa-mars-double:before {
1767 | content: "\f227";
1768 | }
1769 | .fa-venus-mars:before {
1770 | content: "\f228";
1771 | }
1772 | .fa-mars-stroke:before {
1773 | content: "\f229";
1774 | }
1775 | .fa-mars-stroke-v:before {
1776 | content: "\f22a";
1777 | }
1778 | .fa-mars-stroke-h:before {
1779 | content: "\f22b";
1780 | }
1781 | .fa-neuter:before {
1782 | content: "\f22c";
1783 | }
1784 | .fa-genderless:before {
1785 | content: "\f22d";
1786 | }
1787 | .fa-facebook-official:before {
1788 | content: "\f230";
1789 | }
1790 | .fa-pinterest-p:before {
1791 | content: "\f231";
1792 | }
1793 | .fa-whatsapp:before {
1794 | content: "\f232";
1795 | }
1796 | .fa-server:before {
1797 | content: "\f233";
1798 | }
1799 | .fa-user-plus:before {
1800 | content: "\f234";
1801 | }
1802 | .fa-user-times:before {
1803 | content: "\f235";
1804 | }
1805 | .fa-hotel:before,
1806 | .fa-bed:before {
1807 | content: "\f236";
1808 | }
1809 | .fa-viacoin:before {
1810 | content: "\f237";
1811 | }
1812 | .fa-train:before {
1813 | content: "\f238";
1814 | }
1815 | .fa-subway:before {
1816 | content: "\f239";
1817 | }
1818 | .fa-medium:before {
1819 | content: "\f23a";
1820 | }
1821 | .fa-yc:before,
1822 | .fa-y-combinator:before {
1823 | content: "\f23b";
1824 | }
1825 | .fa-optin-monster:before {
1826 | content: "\f23c";
1827 | }
1828 | .fa-opencart:before {
1829 | content: "\f23d";
1830 | }
1831 | .fa-expeditedssl:before {
1832 | content: "\f23e";
1833 | }
1834 | .fa-battery-4:before,
1835 | .fa-battery:before,
1836 | .fa-battery-full:before {
1837 | content: "\f240";
1838 | }
1839 | .fa-battery-3:before,
1840 | .fa-battery-three-quarters:before {
1841 | content: "\f241";
1842 | }
1843 | .fa-battery-2:before,
1844 | .fa-battery-half:before {
1845 | content: "\f242";
1846 | }
1847 | .fa-battery-1:before,
1848 | .fa-battery-quarter:before {
1849 | content: "\f243";
1850 | }
1851 | .fa-battery-0:before,
1852 | .fa-battery-empty:before {
1853 | content: "\f244";
1854 | }
1855 | .fa-mouse-pointer:before {
1856 | content: "\f245";
1857 | }
1858 | .fa-i-cursor:before {
1859 | content: "\f246";
1860 | }
1861 | .fa-object-group:before {
1862 | content: "\f247";
1863 | }
1864 | .fa-object-ungroup:before {
1865 | content: "\f248";
1866 | }
1867 | .fa-sticky-note:before {
1868 | content: "\f249";
1869 | }
1870 | .fa-sticky-note-o:before {
1871 | content: "\f24a";
1872 | }
1873 | .fa-cc-jcb:before {
1874 | content: "\f24b";
1875 | }
1876 | .fa-cc-diners-club:before {
1877 | content: "\f24c";
1878 | }
1879 | .fa-clone:before {
1880 | content: "\f24d";
1881 | }
1882 | .fa-balance-scale:before {
1883 | content: "\f24e";
1884 | }
1885 | .fa-hourglass-o:before {
1886 | content: "\f250";
1887 | }
1888 | .fa-hourglass-1:before,
1889 | .fa-hourglass-start:before {
1890 | content: "\f251";
1891 | }
1892 | .fa-hourglass-2:before,
1893 | .fa-hourglass-half:before {
1894 | content: "\f252";
1895 | }
1896 | .fa-hourglass-3:before,
1897 | .fa-hourglass-end:before {
1898 | content: "\f253";
1899 | }
1900 | .fa-hourglass:before {
1901 | content: "\f254";
1902 | }
1903 | .fa-hand-grab-o:before,
1904 | .fa-hand-rock-o:before {
1905 | content: "\f255";
1906 | }
1907 | .fa-hand-stop-o:before,
1908 | .fa-hand-paper-o:before {
1909 | content: "\f256";
1910 | }
1911 | .fa-hand-scissors-o:before {
1912 | content: "\f257";
1913 | }
1914 | .fa-hand-lizard-o:before {
1915 | content: "\f258";
1916 | }
1917 | .fa-hand-spock-o:before {
1918 | content: "\f259";
1919 | }
1920 | .fa-hand-pointer-o:before {
1921 | content: "\f25a";
1922 | }
1923 | .fa-hand-peace-o:before {
1924 | content: "\f25b";
1925 | }
1926 | .fa-trademark:before {
1927 | content: "\f25c";
1928 | }
1929 | .fa-registered:before {
1930 | content: "\f25d";
1931 | }
1932 | .fa-creative-commons:before {
1933 | content: "\f25e";
1934 | }
1935 | .fa-gg:before {
1936 | content: "\f260";
1937 | }
1938 | .fa-gg-circle:before {
1939 | content: "\f261";
1940 | }
1941 | .fa-tripadvisor:before {
1942 | content: "\f262";
1943 | }
1944 | .fa-odnoklassniki:before {
1945 | content: "\f263";
1946 | }
1947 | .fa-odnoklassniki-square:before {
1948 | content: "\f264";
1949 | }
1950 | .fa-get-pocket:before {
1951 | content: "\f265";
1952 | }
1953 | .fa-wikipedia-w:before {
1954 | content: "\f266";
1955 | }
1956 | .fa-safari:before {
1957 | content: "\f267";
1958 | }
1959 | .fa-chrome:before {
1960 | content: "\f268";
1961 | }
1962 | .fa-firefox:before {
1963 | content: "\f269";
1964 | }
1965 | .fa-opera:before {
1966 | content: "\f26a";
1967 | }
1968 | .fa-internet-explorer:before {
1969 | content: "\f26b";
1970 | }
1971 | .fa-tv:before,
1972 | .fa-television:before {
1973 | content: "\f26c";
1974 | }
1975 | .fa-contao:before {
1976 | content: "\f26d";
1977 | }
1978 | .fa-500px:before {
1979 | content: "\f26e";
1980 | }
1981 | .fa-amazon:before {
1982 | content: "\f270";
1983 | }
1984 | .fa-calendar-plus-o:before {
1985 | content: "\f271";
1986 | }
1987 | .fa-calendar-minus-o:before {
1988 | content: "\f272";
1989 | }
1990 | .fa-calendar-times-o:before {
1991 | content: "\f273";
1992 | }
1993 | .fa-calendar-check-o:before {
1994 | content: "\f274";
1995 | }
1996 | .fa-industry:before {
1997 | content: "\f275";
1998 | }
1999 | .fa-map-pin:before {
2000 | content: "\f276";
2001 | }
2002 | .fa-map-signs:before {
2003 | content: "\f277";
2004 | }
2005 | .fa-map-o:before {
2006 | content: "\f278";
2007 | }
2008 | .fa-map:before {
2009 | content: "\f279";
2010 | }
2011 | .fa-commenting:before {
2012 | content: "\f27a";
2013 | }
2014 | .fa-commenting-o:before {
2015 | content: "\f27b";
2016 | }
2017 | .fa-houzz:before {
2018 | content: "\f27c";
2019 | }
2020 | .fa-vimeo:before {
2021 | content: "\f27d";
2022 | }
2023 | .fa-black-tie:before {
2024 | content: "\f27e";
2025 | }
2026 | .fa-fonticons:before {
2027 | content: "\f280";
2028 | }
2029 | .fa-reddit-alien:before {
2030 | content: "\f281";
2031 | }
2032 | .fa-edge:before {
2033 | content: "\f282";
2034 | }
2035 | .fa-credit-card-alt:before {
2036 | content: "\f283";
2037 | }
2038 | .fa-codiepie:before {
2039 | content: "\f284";
2040 | }
2041 | .fa-modx:before {
2042 | content: "\f285";
2043 | }
2044 | .fa-fort-awesome:before {
2045 | content: "\f286";
2046 | }
2047 | .fa-usb:before {
2048 | content: "\f287";
2049 | }
2050 | .fa-product-hunt:before {
2051 | content: "\f288";
2052 | }
2053 | .fa-mixcloud:before {
2054 | content: "\f289";
2055 | }
2056 | .fa-scribd:before {
2057 | content: "\f28a";
2058 | }
2059 | .fa-pause-circle:before {
2060 | content: "\f28b";
2061 | }
2062 | .fa-pause-circle-o:before {
2063 | content: "\f28c";
2064 | }
2065 | .fa-stop-circle:before {
2066 | content: "\f28d";
2067 | }
2068 | .fa-stop-circle-o:before {
2069 | content: "\f28e";
2070 | }
2071 | .fa-shopping-bag:before {
2072 | content: "\f290";
2073 | }
2074 | .fa-shopping-basket:before {
2075 | content: "\f291";
2076 | }
2077 | .fa-hashtag:before {
2078 | content: "\f292";
2079 | }
2080 | .fa-bluetooth:before {
2081 | content: "\f293";
2082 | }
2083 | .fa-bluetooth-b:before {
2084 | content: "\f294";
2085 | }
2086 | .fa-percent:before {
2087 | content: "\f295";
2088 | }
2089 | .fa-gitlab:before {
2090 | content: "\f296";
2091 | }
2092 | .fa-wpbeginner:before {
2093 | content: "\f297";
2094 | }
2095 | .fa-wpforms:before {
2096 | content: "\f298";
2097 | }
2098 | .fa-envira:before {
2099 | content: "\f299";
2100 | }
2101 | .fa-universal-access:before {
2102 | content: "\f29a";
2103 | }
2104 | .fa-wheelchair-alt:before {
2105 | content: "\f29b";
2106 | }
2107 | .fa-question-circle-o:before {
2108 | content: "\f29c";
2109 | }
2110 | .fa-blind:before {
2111 | content: "\f29d";
2112 | }
2113 | .fa-audio-description:before {
2114 | content: "\f29e";
2115 | }
2116 | .fa-volume-control-phone:before {
2117 | content: "\f2a0";
2118 | }
2119 | .fa-braille:before {
2120 | content: "\f2a1";
2121 | }
2122 | .fa-assistive-listening-systems:before {
2123 | content: "\f2a2";
2124 | }
2125 | .fa-asl-interpreting:before,
2126 | .fa-american-sign-language-interpreting:before {
2127 | content: "\f2a3";
2128 | }
2129 | .fa-deafness:before,
2130 | .fa-hard-of-hearing:before,
2131 | .fa-deaf:before {
2132 | content: "\f2a4";
2133 | }
2134 | .fa-glide:before {
2135 | content: "\f2a5";
2136 | }
2137 | .fa-glide-g:before {
2138 | content: "\f2a6";
2139 | }
2140 | .fa-signing:before,
2141 | .fa-sign-language:before {
2142 | content: "\f2a7";
2143 | }
2144 | .fa-low-vision:before {
2145 | content: "\f2a8";
2146 | }
2147 | .fa-viadeo:before {
2148 | content: "\f2a9";
2149 | }
2150 | .fa-viadeo-square:before {
2151 | content: "\f2aa";
2152 | }
2153 | .fa-snapchat:before {
2154 | content: "\f2ab";
2155 | }
2156 | .fa-snapchat-ghost:before {
2157 | content: "\f2ac";
2158 | }
2159 | .fa-snapchat-square:before {
2160 | content: "\f2ad";
2161 | }
2162 | .fa-pied-piper:before {
2163 | content: "\f2ae";
2164 | }
2165 | .fa-first-order:before {
2166 | content: "\f2b0";
2167 | }
2168 | .fa-yoast:before {
2169 | content: "\f2b1";
2170 | }
2171 | .fa-themeisle:before {
2172 | content: "\f2b2";
2173 | }
2174 | .fa-google-plus-circle:before,
2175 | .fa-google-plus-official:before {
2176 | content: "\f2b3";
2177 | }
2178 | .fa-fa:before,
2179 | .fa-font-awesome:before {
2180 | content: "\f2b4";
2181 | }
2182 | .fa-handshake-o:before {
2183 | content: "\f2b5";
2184 | }
2185 | .fa-envelope-open:before {
2186 | content: "\f2b6";
2187 | }
2188 | .fa-envelope-open-o:before {
2189 | content: "\f2b7";
2190 | }
2191 | .fa-linode:before {
2192 | content: "\f2b8";
2193 | }
2194 | .fa-address-book:before {
2195 | content: "\f2b9";
2196 | }
2197 | .fa-address-book-o:before {
2198 | content: "\f2ba";
2199 | }
2200 | .fa-vcard:before,
2201 | .fa-address-card:before {
2202 | content: "\f2bb";
2203 | }
2204 | .fa-vcard-o:before,
2205 | .fa-address-card-o:before {
2206 | content: "\f2bc";
2207 | }
2208 | .fa-user-circle:before {
2209 | content: "\f2bd";
2210 | }
2211 | .fa-user-circle-o:before {
2212 | content: "\f2be";
2213 | }
2214 | .fa-user-o:before {
2215 | content: "\f2c0";
2216 | }
2217 | .fa-id-badge:before {
2218 | content: "\f2c1";
2219 | }
2220 | .fa-drivers-license:before,
2221 | .fa-id-card:before {
2222 | content: "\f2c2";
2223 | }
2224 | .fa-drivers-license-o:before,
2225 | .fa-id-card-o:before {
2226 | content: "\f2c3";
2227 | }
2228 | .fa-quora:before {
2229 | content: "\f2c4";
2230 | }
2231 | .fa-free-code-camp:before {
2232 | content: "\f2c5";
2233 | }
2234 | .fa-telegram:before {
2235 | content: "\f2c6";
2236 | }
2237 | .fa-thermometer-4:before,
2238 | .fa-thermometer:before,
2239 | .fa-thermometer-full:before {
2240 | content: "\f2c7";
2241 | }
2242 | .fa-thermometer-3:before,
2243 | .fa-thermometer-three-quarters:before {
2244 | content: "\f2c8";
2245 | }
2246 | .fa-thermometer-2:before,
2247 | .fa-thermometer-half:before {
2248 | content: "\f2c9";
2249 | }
2250 | .fa-thermometer-1:before,
2251 | .fa-thermometer-quarter:before {
2252 | content: "\f2ca";
2253 | }
2254 | .fa-thermometer-0:before,
2255 | .fa-thermometer-empty:before {
2256 | content: "\f2cb";
2257 | }
2258 | .fa-shower:before {
2259 | content: "\f2cc";
2260 | }
2261 | .fa-bathtub:before,
2262 | .fa-s15:before,
2263 | .fa-bath:before {
2264 | content: "\f2cd";
2265 | }
2266 | .fa-podcast:before {
2267 | content: "\f2ce";
2268 | }
2269 | .fa-window-maximize:before {
2270 | content: "\f2d0";
2271 | }
2272 | .fa-window-minimize:before {
2273 | content: "\f2d1";
2274 | }
2275 | .fa-window-restore:before {
2276 | content: "\f2d2";
2277 | }
2278 | .fa-times-rectangle:before,
2279 | .fa-window-close:before {
2280 | content: "\f2d3";
2281 | }
2282 | .fa-times-rectangle-o:before,
2283 | .fa-window-close-o:before {
2284 | content: "\f2d4";
2285 | }
2286 | .fa-bandcamp:before {
2287 | content: "\f2d5";
2288 | }
2289 | .fa-grav:before {
2290 | content: "\f2d6";
2291 | }
2292 | .fa-etsy:before {
2293 | content: "\f2d7";
2294 | }
2295 | .fa-imdb:before {
2296 | content: "\f2d8";
2297 | }
2298 | .fa-ravelry:before {
2299 | content: "\f2d9";
2300 | }
2301 | .fa-eercast:before {
2302 | content: "\f2da";
2303 | }
2304 | .fa-microchip:before {
2305 | content: "\f2db";
2306 | }
2307 | .fa-snowflake-o:before {
2308 | content: "\f2dc";
2309 | }
2310 | .fa-superpowers:before {
2311 | content: "\f2dd";
2312 | }
2313 | .fa-wpexplorer:before {
2314 | content: "\f2de";
2315 | }
2316 | .fa-meetup:before {
2317 | content: "\f2e0";
2318 | }
2319 | .sr-only {
2320 | position: absolute;
2321 | width: 1px;
2322 | height: 1px;
2323 | padding: 0;
2324 | margin: -1px;
2325 | overflow: hidden;
2326 | clip: rect(0, 0, 0, 0);
2327 | border: 0;
2328 | }
2329 | .sr-only-focusable:active,
2330 | .sr-only-focusable:focus {
2331 | position: static;
2332 | width: auto;
2333 | height: auto;
2334 | margin: 0;
2335 | overflow: visible;
2336 | clip: auto;
2337 | }
2338 |
--------------------------------------------------------------------------------