Repository: meetyan/raise Branch: master Commit: 7fd970899f2c Files: 79 Total size: 98.7 KB Directory structure: gitextract_6pndr1_7/ ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── build/ │ ├── after-sign-hook.js │ ├── icon.icns │ └── mac/ │ └── entitlements.plist ├── chrome-manifest.js ├── electron/ │ ├── common.js │ ├── config/ │ │ ├── constants.js │ │ ├── index.js │ │ └── store.js │ ├── ipc/ │ │ └── index.js │ ├── main.js │ ├── preload.js │ └── update-manager.js ├── jsconfig.json ├── package.json ├── shared/ │ ├── constants.js │ └── index.js ├── src/ │ ├── app-context.js │ ├── app.js │ ├── app.scss │ ├── assets/ │ │ └── styles/ │ │ ├── global.scss │ │ └── reset.scss │ ├── chrome/ │ │ ├── plausible.js │ │ └── popup.js │ ├── components/ │ │ ├── about-modal/ │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── developer-content/ │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── filter/ │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── index.js │ │ ├── raise-header/ │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── repository-content/ │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── settings-modal/ │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── skeleton-placeholder/ │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── update-notification/ │ │ │ ├── index.js │ │ │ └── styles.scss │ │ └── upper-container/ │ │ └── index.js │ ├── config/ │ │ ├── constants.js │ │ ├── index.js │ │ ├── languages.js │ │ └── spoken-languages.js │ ├── hooks/ │ │ ├── index.js │ │ ├── use-context-props.js │ │ ├── use-dock-icon.js │ │ ├── use-mode.js │ │ ├── use-outside-click.js │ │ └── use-scroll-position.js │ ├── index.ejs │ ├── index.js │ ├── io/ │ │ ├── index.js │ │ ├── interceptor.js │ │ └── trending.js │ ├── lib/ │ │ ├── index.js │ │ └── is-electron.js │ ├── pages/ │ │ └── index/ │ │ ├── index.js │ │ └── styles.scss │ └── utils/ │ ├── common.js │ ├── index.js │ └── polyfill/ │ ├── electron/ │ │ ├── index.js │ │ ├── storage.js │ │ └── utils.js │ ├── index.js │ └── web/ │ ├── index.js │ ├── storage.js │ └── utils.js └── webpack/ ├── chrome/ │ └── webpack.config.js ├── main/ │ └── webpack.config.js ├── renderer/ │ └── webpack.config.js └── webpack.base.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.js ================================================ module.exports = { root: true, env: { browser: true, node: true, commonjs: true, }, parser: '@babel/eslint-parser', parserOptions: { sourceType: 'module', ecmaFeatures: {jsx: true}, }, settings: { 'import/resolver': { alias: { map: [ ['@', './src'], ['@static', './static'], ['@shared', './shared'], ['@pkg', './package.json'], ], extensions: ['.js', '.jsx'], }, }, react: {version: 'detect'}, }, extends: [ 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:import/recommended', 'standard', 'plugin:prettier/recommended', ], globals: { chrome: true, }, rules: { 'react/react-in-jsx-scope': 'off', 'prettier/prettier': [ 'error', { arrowParens: 'avoid', bracketSpacing: false, printWidth: 100, semi: false, singleQuote: true, endOfLine: 'auto', }, ], 'react/prop-types': 'off', 'react-hooks/exhaustive-deps': 'off', 'prefer-promise-reject-errors': [2, {allowEmptyReject: true}], camelcase: ['error', {properties: 'never', ignoreDestructuring: true}], }, } ================================================ FILE: .gitignore ================================================ node_modules dist out src/chrome/manifest.json .DS_Store .eslintcache .stylelintcache .env ================================================ FILE: .prettierrc ================================================ { "arrowParens": "avoid", "bracketSpacing": false, "printWidth": 100, "semi": false, "singleQuote": true } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Jiajun Yan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Raise A simple (and unofficial) GitHub Trending client that lives in your menubar. ![Raise App Screenshots](./static/screenshots/banner.png) ## 📸 Screenshots ![Raise App Screenshots](./static/screenshots/ui.png) ## 🖥 Installation ### New!! Raise is now available as a Chrome Extension More in favor of a Chrome Extension variant? Check it out here: Otherwise, download from [GitHub Releases](https://github.com/meetyan/raise/releases) and install. Currently Raise can run on macOS and Windows machines. ### macOS If you use an Intel machine, please download the `.zip` file with its filename containing no architecture. Otherwise use `arm64.zip` if your hardware is armed with Apple Silicon (M1/M2). ### Windows For Windows users simply download the package with `.exe` extension. If it's your first time to open Raise, you might see a screen saying `Windows protected your PC. Windows SmartScreen prevented an unrecognized app from start. Running this app might put your PC at risk.`. To bypass it, click `More Info` and then click `Run anyway`. This is simply because Raise on Windows is not yet [code signed](https://www.electronjs.org/docs/latest/tutorial/code-signing). Read [this](https://stackoverflow.com/questions/48946680/how-to-avoid-the-windows-defender-smartscreen-prevented-an-unrecognized-app-fro) for your information. ## 🙌🏻 Features - 🌠 Showcasing GitHub's trending repos and developers - 🗺 Simple and intuitive user interface - 🌍 Language and date range filtering - 🌗 Dark mode - 💻 More under development ## 🛠 Tech Involved - [Electron](https://electronjs.org/) - [React](https://reactjs.org/) - [Semi Design](https://semi.design/) - [GitHub Trending API](https://github.com/huchenme/github-trending-api) - [Plausible](https://plausible.io/) - [PM2](https://pm2.keymetrics.io/) - [Webpack](https://webpack.js.org/) ## 🧑🏻‍💻 How to Develop Raise is developed on Node.js v16. Other Node.js versions have not been tested. Run the following commands in `Terminal.app` on macOS or `PowerShell` on Windows: ```bash yarn yarn start ``` ## 📢 Build and Deploy To build and deploy, run the following: ```bash yarn build yarn release ``` ================================================ FILE: babel.config.js ================================================ module.exports = { presets: [['@babel/preset-env'], '@babel/preset-react'], plugins: [ '@babel/plugin-proposal-class-properties', '@babel/plugin-syntax-dynamic-import', [ 'import', {libraryName: 'licia', libraryDirectory: '', camel2DashComponentName: false}, 'licia', ], ], } ================================================ FILE: build/after-sign-hook.js ================================================ // See https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/ // See https://philo.dev/notarizing-your-electron-application/ require('dotenv').config() const fs = require('fs') const path = require('path') const {notarize} = require('electron-notarize') const pkg = require('../package.json') module.exports = async function (params) { if (process.platform !== 'darwin') { return } console.log('afterSign hook triggered', params) const appId = pkg.build.appId const appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`) if (!fs.existsSync(appPath)) { console.log('skip') return } console.log(`Notarizing ${appId} found at ${appPath}`) try { await notarize({ appBundleId: appId, appPath: appPath, appleId: process.env.APPLE_ID, appleIdPassword: process.env.APPLE_ID_PASSWORD, }) } catch (error) { console.error(error) } console.log(`Done notarizing ${appId}`) } ================================================ FILE: build/mac/entitlements.plist ================================================ com.apple.security.cs.allow-unsigned-executable-memory ================================================ FILE: chrome-manifest.js ================================================ /** * Dynamically generate manifest.json for Chrome Extension */ const fs = require('fs') const path = require('path') const pkg = require('./package.json') module.exports = () => { const manifest = { name: pkg.chromeProductName, description: pkg.chromeDescription, version: pkg.version, manifest_version: 3, permissions: [], action: { default_popup: 'index.html', default_icon: { 16: '/static/16.png', 32: '/static/32.png', 48: '/static/48.png', 128: '/static/128.png', }, }, icons: { 16: '/static/16.png', 32: '/static/32.png', 48: '/static/48.png', 128: '/static/128.png', }, } fs.writeFileSync(path.resolve('./src/chrome/manifest.json'), JSON.stringify(manifest)) } ================================================ FILE: electron/common.js ================================================ import {app, BrowserWindow, Menu, shell, Tray} from 'electron' import path from 'path' import log from 'electron-log' import {IPC_FUNCTION, STORAGE_KEY} from '@shared' import pkg from '@pkg' import {INDEX_URL, isMac, ICON, MENUBAR, store, isDev} from './config' import {mb} from './main' // See https://github.com/electron/electron/issues/19775. process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true' export const browserWindowConfig = { width: MENUBAR.WIDTH, height: MENUBAR.HEIGHT, webPreferences: {preload: path.join(__dirname, './preload.js')}, } export const createWindow = () => { const mainWindow = new BrowserWindow({ ...browserWindowConfig, icon: ICON.LOGO, }) mainWindow.webContents.openDevTools() mainWindow.loadURL(INDEX_URL.DEV) return mainWindow } export const createMenu = () => { const template = [ ...(isMac ? [ { label: app.name, submenu: [ { label: `About ${pkg.productName}`, click: () => { mb.showWindow() mb.window.send(IPC_FUNCTION.SHOW_ABOUT_MODAL) }, }, {type: 'separator'}, { label: 'Preferences', click: () => { mb.showWindow() mb.window.send(IPC_FUNCTION.SHOW_SETTINGS_MODAL) }, }, {type: 'separator'}, {role: 'hide'}, {role: 'hideOthers'}, {role: 'unhide'}, {type: 'separator'}, {role: 'quit'}, ], }, ] : []), { label: 'View', submenu: [ {role: 'reload'}, {role: 'forceReload'}, ...(isDev ? [{role: 'toggleDevTools'}] : []), ], }, { role: 'help', submenu: [ { label: 'Website', click: async () => { await shell.openExternal(pkg.repository) }, }, ], }, ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) } /** * Creates a right-clickable tray * See https://erikmartinjordan.com/menu-contextual-electron */ export const createTray = () => { const tray = new Tray(ICON.MENU) const contextMenu = Menu.buildFromTemplate([{role: 'quit'}]) tray.on('right-click', () => tray.popUpContextMenu(contextMenu)) return tray } export const showDockIconAtLogin = () => { if (!isMac) return const shouldShowDockIcon = store.get(STORAGE_KEY.SHOW_DOCK_ICON) log.info('shouldShowDockIcon', shouldShowDockIcon) // Dock icon persists in the dock except a user disables it if (shouldShowDockIcon === false) { app.dock.hide() } } ================================================ FILE: electron/config/constants.js ================================================ import path from 'path' import is from 'electron-is' export const BROWSER_WINDOW = { WIDTH: 1440, HEIGHT: 900, } export const MENUBAR = { WIDTH: 400, HEIGHT: 600, } export const INDEX_URL = { DEV: 'http://localhost:3000', PROD: `file://${path.join(__dirname, './index.html')}`, } export const isMac = is.macOS() export const ICON = { LOGO: path.join(__dirname, './static/logo.png'), MENU: path.join(__dirname, `./static/${isMac ? 'menu-' : ''}logo.png`), } export const INTERVAL = { UPDATE: 1000 * 60 * 60 * 24, // every 24 hours } export const isDev = is.dev() ================================================ FILE: electron/config/index.js ================================================ export * from './constants' export * from './store' ================================================ FILE: electron/config/store.js ================================================ import Store from 'electron-store' import {STORAGE_KEY} from '@shared' export const store = new Store({ defaults: { [STORAGE_KEY.ENABLE_AUTO_UPDATE]: true, }, }) ================================================ FILE: electron/ipc/index.js ================================================ import {app} from 'electron' import {autoUpdater} from 'electron-updater' import {isMac} from '../config' export const handleShowDockIcon = (_, visible) => { if (!isMac) return if (visible) { app.dock.show() return } app.dock.hide() } export const handleQuitAndInstall = () => { autoUpdater.quitAndInstall() } ================================================ FILE: electron/main.js ================================================ import {app, ipcMain} from 'electron' import {menubar} from 'menubar' import {IPC_FUNCTION} from '@shared' import pkg from '@pkg' import {INDEX_URL, isMac, ICON, isDev} from './config' import {handleQuitAndInstall, handleShowDockIcon} from './ipc' import { browserWindowConfig, createMenu, createTray, // eslint-disable-next-line no-unused-vars createWindow, showDockIconAtLogin, } from './common' import updateManager from './update-manager' app.setName(pkg.productName) export let mb = null let isFirstLoad = true /** * Shows app icon in dock on macOS */ if (isMac) { app.dock.setIcon(ICON.LOGO) app.dock.show() } app.whenReady().then(() => { ipcMain.on(IPC_FUNCTION.SHOW_DOCK_ICON, handleShowDockIcon) ipcMain.on(IPC_FUNCTION.QUIT_AND_INSTALL, handleQuitAndInstall) mb = menubar({ icon: ICON.MENU, index: isDev ? INDEX_URL.DEV : INDEX_URL.PROD, browserWindow: {...browserWindowConfig, resizable: false}, preloadWindow: true, tray: createTray(), tooltip: pkg.productName, }) createMenu(mb) mb.on('ready', () => { updateManager.init() showDockIconAtLogin() if (isDev) { createWindow() // enable this if you need an extra window open } /** * The setTimeout is used as a hack to show window on ready. * Otherwise the window simply flashes and won't stay shown. * See https://github.com/maxogden/menubar/issues/76. */ setTimeout(() => { mb.showWindow() }, 500) }) mb.on('show', () => { /** * Reloads page after a long period of inactivity. * Checks on every show() call (except when the app loads for the very first time). */ if (isFirstLoad) return (isFirstLoad = false) mb.window.send(IPC_FUNCTION.RELOAD_AFTER_INACTIVITY) }) }) ================================================ FILE: electron/preload.js ================================================ import {contextBridge, ipcRenderer, shell} from 'electron' import {IPC_FUNCTION} from '@shared' import {store} from './config' contextBridge.exposeInMainWorld('electron', { storage: { set: (key, val) => store.set(key, val), get: key => store.get(key), store: () => store.store, }, open: url => shell.openExternal(url), /** * Wraps commonly used ipcRenderer methods with the followings. * See: https://github.com/reZach/secure-electron-template/issues/43#issuecomment-772303787 */ send: (channel, data) => { if (Object.values(IPC_FUNCTION).includes(channel)) { ipcRenderer.send(channel, data) } }, receive: (channel, func) => { if (Object.values(IPC_FUNCTION).includes(channel)) { const subscription = (_, ...args) => func(...args) ipcRenderer.on(channel, subscription) return () => { ipcRenderer.removeListener(channel, subscription) } } }, receiveOnce: (channel, func) => { if (Object.values(IPC_FUNCTION).includes(channel)) { ipcRenderer.once(channel, (event, ...args) => func(...args)) } }, removeAllListeners: channel => { if (Object.values(IPC_FUNCTION).includes(channel)) { ipcRenderer.removeAllListeners(channel) } }, }) ================================================ FILE: electron/update-manager.js ================================================ import {autoUpdater} from 'electron-updater' import log from 'electron-log' import {IPC_FUNCTION, STORAGE_KEY} from '@shared' import {store, INTERVAL} from './config' import {mb} from './main' autoUpdater.logger = log autoUpdater.logger.transports.file.level = 'info' const onUpdateDownloaded = () => { autoUpdater.on('update-downloaded', () => { mb.window.send(IPC_FUNCTION.SHOW_UPDATE_NOTIFICATION) }) } const checkForUpdates = () => { log.info('store info', store.store) const shouldAutoUpdate = store.get(STORAGE_KEY.ENABLE_AUTO_UPDATE) log.info('shouldAutoUpdate', shouldAutoUpdate) if (!shouldAutoUpdate) { log.info('AUTO_UPDATE is set to false. Abort auto update...') return } autoUpdater.checkForUpdates() } const init = () => { checkForUpdates() /** * Sets interval for periodical checks */ setInterval(checkForUpdates, INTERVAL.UPDATE) /** * Updater events */ onUpdateDownloaded() } export default {init, checkForUpdates} ================================================ FILE: jsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"], "@static/*": ["./static/*"], "@shared": ["./shared"], "@pkg": ["./package.json"] } }, "typeAcquisition": {"include": ["chrome"]}, "exclude": ["node_modules", "dist"] } ================================================ FILE: package.json ================================================ { "name": "raise", "productName": "Raise", "chromeProductName": "Raise - GitHub Trending", "version": "1.2.1", "description": "A simple (and unofficial) GitHub Trending client that lives in your menubar", "chromeDescription": "GitHub Trending at a glance", "main": "dist/main.js", "repository": "https://github.com/meetyan/raise.git", "homepage": "./", "author": "Jiajun Yan", "license": "MIT", "scripts": { "start": "concurrently -k \"yarn start:main\" \"yarn start:renderer\"", "start:main": "webpack --mode=development --config webpack/main/webpack.config.js && wait-on tcp:3000 && electron .", "start:renderer": "webpack server --mode=development --config webpack/renderer/webpack.config.js --hot", "start:chrome": "webpack --mode=development --config webpack/chrome/webpack.config.js --watch", "build": "rimraf ./dist && yarn build:main && yarn build:renderer", "build:main": "webpack --mode=production --config webpack/main/webpack.config.js --progress", "build:renderer": "webpack --mode=production --config webpack/renderer/webpack.config.js --progress", "build:chrome": "rimraf ./dist && webpack --mode=production --config webpack/chrome/webpack.config.js --progress", "lint": "eslint 'src/**/*.{js,jsx}' 'electron/**/*.{js,jsx}' --cache --fix", "package": "rimraf ./out && electron-builder build --mac --win --publish never", "release": "rimraf ./out && electron-builder build --mac --win --publish always" }, "dependencies": { "@douyinfe/semi-icons": "^2.15.1", "@douyinfe/semi-illustrations": "^2.16.0", "@douyinfe/semi-ui": "^2.15.1", "ahooks": "^3.7.0", "axios": "^0.27.2", "electron-is": "^3.0.0", "electron-log": "^4.4.8", "electron-store": "^8.1.0", "electron-updater": "^5.2.1", "lodash": "^4.17.21", "menubar": "^9.2.1", "nprogress": "^0.2.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@babel/core": "^7.17.4", "@babel/eslint-parser": "7.15.4", "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", "babel-loader": "^8.2.3", "babel-plugin-import": "1.13.3", "concurrently": "^7.3.0", "copy-webpack-plugin": "10.2.0", "css-loader": "^6.6.0", "css-minimizer-webpack-plugin": "3.3.1", "electron": "^19.0.9", "electron-builder": "^23.3.3", "electron-notarize": "^1.2.1", "eslint": "7.32.0", "eslint-config-prettier": "8.3.0", "eslint-config-standard": "16.0.3", "eslint-import-resolver-alias": "1.1.2", "eslint-plugin-import": "^2.12.0", "eslint-plugin-node": "11.1.0", "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-promise": "5.1.0", "eslint-plugin-react": "^7.8.2", "eslint-plugin-react-hooks": "^4.2.0", "file-loader": "5.1.0", "html-webpack-plugin": "^5.5.0", "mini-css-extract-plugin": "2.4.5", "node-loader": "^2.0.0", "node-sass": "^7.0.1", "postcss": "8", "postcss-loader": "^6.2.1", "prettier": "^2.5.1", "react-dev-utils": "12.0.0", "rimraf": "^3.0.2", "sass-loader": "^12.6.0", "style-loader": "^3.3.1", "wait-on": "^6.0.1", "webpack": "^5.69.0", "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.7.4", "webpack-merge": "^5.8.0" }, "build": { "productName": "Raise", "appId": "to.curve.raise", "afterSign": "./build/after-sign-hook.js", "directories": { "output": "out" }, "mac": { "mergeASARs": false, "target": [ { "target": "zip", "arch": [ "x64", "arm64" ] } ], "type": "distribution", "hardenedRuntime": true, "gatekeeperAssess": false, "entitlements": "build/mac/entitlements.plist", "entitlementsInherit": "build/mac/entitlements.plist", "publish": { "provider": "github", "owner": "meetyan", "repo": "raise" } }, "win": { "target": [ { "target": "nsis", "arch": [ "x64", "ia32" ] } ], "publish": { "provider": "github", "owner": "meetyan", "repo": "raise" } }, "files": [ "dist/**/*", "node_modules/**/*" ], "publish": { "provider": "github", "owner": "meetyan" } } } ================================================ FILE: shared/constants.js ================================================ export const IPC_FUNCTION = { SHOW_ABOUT_MODAL: 'show-about-modal', SHOW_SETTINGS_MODAL: 'show-settings-modal', SHOW_DOCK_ICON: 'show-dock-icon', RELOAD_AFTER_INACTIVITY: 'reload-after-inactivity', SHOW_UPDATE_NOTIFICATION: 'show-update-notification', QUIT_AND_INSTALL: 'quit-and-install', } export const STORAGE_KEY = { MODE: 'mode', SHOW_BACK_TOP: 'show-back-top', SHOW_DOCK_ICON: 'show-dock-icon', TRENDING_TYPE: 'trending-type', ENABLE_AUTO_UPDATE: 'enable-auto-update', } ================================================ FILE: shared/index.js ================================================ export * from './constants' ================================================ FILE: src/app-context.js ================================================ import React, {useState, useContext, useEffect} from 'react' import {polyfill} from './utils' const {setStorage} = polyfill export const AppContext = React.createContext({}) export const AppProvider = ({value, children}) => { const [context, setContext] = useState(value) useEffect(() => { setContext(value) }, [value]) return {children} } export const useAppContext = () => { return useContext(AppContext) } export const useContextProp = propName => { const [context, setContext] = useAppContext() const [prop, _setProp] = useState(context[propName]) useEffect(() => { _setProp(context[propName]) }, [context, propName]) const setProp = val => { _setProp(val) setStorage(propName, val) setContext(preCtx => { const data = { ...preCtx, [propName]: val, } return data }) } return [prop, setProp] } ================================================ FILE: src/app.js ================================================ import React, {useState} from 'react' import {Divider, Layout, Toast, Typography} from '@douyinfe/semi-ui' import {MODE, TRENDING_TYPE, Z_INDEX} from '@/config' import {AppProvider} from '@/app-context' import {UpdateNotification, UpperContainer} from '@/components' import Index from '@/pages/index/index' import {polyfill} from '@/utils' import pkg from '@pkg' import {STORAGE_KEY} from '@shared' import 'nprogress/nprogress.css' import '@/assets/styles/reset.scss' import '@/assets/styles/global.scss' import styles from '@/app.scss' const {Text} = Typography const {Footer} = Layout Toast.config({zIndex: Z_INDEX.TOAST}) const {REPOSITORIES} = TRENDING_TYPE const {getContextFromStorage} = polyfill const App = () => { const [context] = useState({ [STORAGE_KEY.MODE]: MODE.LIGHT, // system themes [STORAGE_KEY.SHOW_BACK_TOP]: true, [STORAGE_KEY.SHOW_DOCK_ICON]: true, [STORAGE_KEY.ENABLE_AUTO_UPDATE]: true, ...getContextFromStorage(), [STORAGE_KEY.TRENDING_TYPE]: REPOSITORIES, }) return (
© {new Date().getFullYear()} {pkg.productName}. All rights reserved.
) } export default App ================================================ FILE: src/app.scss ================================================ ::-webkit-scrollbar { display: none; } .layout { background-color: var(--semi-color-bg-0); padding: 20px; min-width: 400px; } .copyright { padding-top: 10px; display: flex; flex-direction: column; align-items: flex-start; } ================================================ FILE: src/assets/styles/global.scss ================================================ body { color: var(--semi-color-text-0); background-color: var(--semi-color-bg-0); } :global(#nprogress .bar) { z-index: 99999; } ================================================ FILE: src/assets/styles/reset.scss ================================================ /* stylelint-disable */ /* http://meyerweb.com/eric/tools/css/reset/ v5.0.1 | 20191019 License: none (public domain) */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, menu, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, main, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section { display: block; } /* HTML5 hidden-attribute fix for newer browsers */ *[hidden] { display: none; } body { line-height: 1; } menu, ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote::before, blockquote::after, q::before, q::after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; } ================================================ FILE: src/chrome/plausible.js ================================================ // eslint-disable-next-line !function(){"use strict";var a=window.location,r=window.document,t=window.localStorage,o=r.currentScript,s=o.getAttribute("data-api")||new URL(o.src).origin+"/api/event",l=t&&t.plausible_ignore;function p(t){console.warn("Ignoring Event: "+t)}function e(t,e){if(/^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/.test(a.hostname)||"file:"===a.protocol)return p("localhost");if(!(window._phantom||window.__nightmare||window.navigator.webdriver||window.Cypress)){if("true"==l)return p("localStorage flag");var i={};i.n=t,i.u=a.href,i.d=o.getAttribute("data-domain"),i.r=r.referrer||null,i.w=window.innerWidth,e&&e.meta&&(i.m=JSON.stringify(e.meta)),e&&e.props&&(i.p=JSON.stringify(e.props));var n=new XMLHttpRequest;n.open("POST",s,!0),n.setRequestHeader("Content-Type","text/plain"),n.send(JSON.stringify(i)),n.onreadystatechange=function(){4==n.readyState&&e&&e.callback&&e.callback()}}}var i=window.plausible&&window.plausible.q||[];window.plausible=e;for(var n,w=0;w { try { return JSON.parse(window.localStorage.getItem(key)).value } catch (err) { console.log(`An error occurred when getting storage ${key}.`, err) return null } } const mode = getStorage(MODE) document.body.style.backgroundColor = mode === 'dark' ? darkColor : lightColor document.documentElement.style.width = '400px' document.documentElement.style.minHeight = '599.9px' // not sure why setting height to 600px causes page not scrollable ================================================ FILE: src/components/about-modal/index.js ================================================ import React from 'react' import {Divider, Modal, Typography} from '@douyinfe/semi-ui' import {VERSION, Z_INDEX} from '@/config' import pkg from '@pkg' import Logo from '@static/logo-without-padding.png' import {polyfill} from '@/utils' import styles from './styles.scss' const {Text, Title} = Typography const {open} = polyfill const AboutModal = ({visible, setVisible}) => { return ( setVisible(false)} onCancel={() => setVisible(false)} closeOnEsc={true} width={350} height="fit-content" centered footer={null} zIndex={Z_INDEX.MODAL} >
logo {pkg.productName}
Version {VERSION} A simple (and unofficial) GitHub Trending client that lives in your menubar.
open(pkg.repository)}> An open-source project by Jiajun Yan. Copyright © {new Date().getFullYear()} Raise. All rights reserved.
) } export default AboutModal ================================================ FILE: src/components/about-modal/styles.scss ================================================ .about-modal { display: flex; flex-direction: column; padding: 20px 0; .logo { display: flex; flex-direction: column; align-items: center; margin-bottom: 20px; img { width: 80px; height: 80px; } } .title { margin-top: 10px; } .center-aligned { text-align: center; } .version { margin-bottom: 20px; } .copyright { display: flex; flex-direction: column; margin: 20px 0 6px; } } ================================================ FILE: src/components/developer-content/index.js ================================================ import React from 'react' import {Typography, Layout, Card, Space} from '@douyinfe/semi-ui' import {IconBranch, IconCrown} from '@douyinfe/semi-icons' import {SkeletonPlaceholder} from '@/components' import {polyfill} from '@/utils' import styles from './styles.scss' const {Content} = Layout const {Text, Title} = Typography const {open} = polyfill const AuthorHeader = ({item}) => (
open(item.url)} /> open(item.url)}> {item.name} open(item.url)}> {item.username}
) const DeveloperContent = ({list, loading}) => { return ( {Array.from({length: 5}).map((_, index) => ( ))} {list.map(item => { if (!item.repo) { return (
) } return ( } className={styles.developer}> Popular Repo {' '} open(item.repo.url)}> {item.repo.name} {item.repo.description ? ( {item.repo.description} ) : null} ) })}
) } export default DeveloperContent ================================================ FILE: src/components/developer-content/styles.scss ================================================ .content { padding-top: 15px; .developer { width: 100%; margin-bottom: 20px; background-color: var(--semi-color-fill-0); } .header { display: flex; align-items: center; .avatar { width: 50px; height: 50px; border-radius: 50%; margin-right: 10px; cursor: pointer; } } .description { margin-top: 10px; } .cursor { cursor: pointer; } } ================================================ FILE: src/components/filter/index.js ================================================ import React, {forwardRef, useImperativeHandle, useRef} from 'react' import {Form} from '@douyinfe/semi-ui' import {SINCE_ARRAY, SPOKEN_LANGUAGES, LANGUAGES, SINCE, TRENDING_TYPE, Z_INDEX} from '@/config' import {truncate} from '@/utils' import {useTrendingType} from '@/hooks' import styles from './styles.scss' const Filter = ({getList}, ref) => { const api = useRef() const [trendingType] = useTrendingType() const isRepo = trendingType === TRENDING_TYPE.REPOSITORIES useImperativeHandle(ref, () => ({ reset() { api.current.reset() }, })) return (
(api.current = formApi)} > {isRepo ? ( {SPOKEN_LANGUAGES.map(item => { return ( {truncate(item.name)} ) })} ) : null} {LANGUAGES.map(item => { return ( {truncate(item.name)} ) })} {SINCE_ARRAY.map(since => { return ( {since.name} ) })}
) } export default forwardRef(Filter) ================================================ FILE: src/components/filter/styles.scss ================================================ .filter { display: flex; flex-direction: column; .bottom { display: flex; flex-direction: column; .bottom-select { width: 100%; } } .bottom-select-text { display: block; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } } ================================================ FILE: src/components/index.js ================================================ export {default as RaiseHeader} from './raise-header' export {default as DeveloperContent} from './developer-content' export {default as RepositoryContent} from './repository-content' export {default as SettingsModal} from './settings-modal' export {default as AboutModal} from './about-modal' export {default as SkeletonPlaceholder} from './skeleton-placeholder' export {default as Filter} from './filter' export {default as UpperContainer} from './upper-container' export {default as UpdateNotification} from './update-notification' ================================================ FILE: src/components/raise-header/index.js ================================================ import React, {useEffect, useLayoutEffect, useRef, useState} from 'react' import {Button, Collapsible, Divider, Layout, Typography} from '@douyinfe/semi-ui' import { IconFilter, IconInfoCircle, IconMoon, IconRefresh, IconSetting, IconSun, } from '@douyinfe/semi-icons' import {Filter, SettingsModal, AboutModal} from '@/components' import {MODE, TRENDING_TYPE, isMac} from '@/config' import {useMode, useOutsideClick, useScrollPosition, useTrendingType} from '@/hooks' import {IPC_FUNCTION} from '@shared' import pkg from '@pkg' import {polyfill} from '@/utils' import Logo from '@static/logo-without-padding.png' import styles from './styles.scss' const {Header} = Layout const {Text} = Typography const {REPOSITORIES, DEVELOPERS} = TRENDING_TYPE const {SHOW_ABOUT_MODAL, SHOW_SETTINGS_MODAL} = IPC_FUNCTION const RaiseHeader = ({refresh, getList, resetList}) => { const headerRef = useRef() const filterRef = useRef() const [mode, setMode] = useMode() const [trendingType, setTrendingType] = useTrendingType() const scrollPosition = useScrollPosition() const [headerHeight, setHeaderHeight] = useState(0) const [showFilter, setShowFilter] = useState(false) const [settingsModalVisible, setSettingsModalVisible] = useState(false) const [aboutModalVisible, setAboutModalVisible] = useState(false) useOutsideClick(headerRef, () => setShowFilter(false)) const trendingTypeButtonConfig = buttonType => { return trendingType === buttonType ? {type: 'primary', theme: 'solid'} : {} } const TrendingButton = ({type}) => { return ( ) } const toggleFilter = () => { setShowFilter(!showFilter) } useLayoutEffect(() => { const [headerComponent] = document.getElementsByClassName(styles.header) setHeaderHeight(headerComponent.offsetHeight - 20) }, []) useEffect(() => { setShowFilter(false) filterRef.current.reset() }, [trendingType]) useEffect(() => { const {receive} = polyfill receive(SHOW_ABOUT_MODAL, () => setAboutModalVisible(true)) receive(SHOW_SETTINGS_MODAL, () => setSettingsModalVisible(true)) }, []) return ( <>

GitHub Trending

logo {pkg.productName}
) } export default RaiseHeader ================================================ FILE: src/components/raise-header/styles.scss ================================================ .header { display: flex; flex-direction: column; width: 100%; padding: 20px 20px 0; position: fixed; top: 0; left: 0; z-index: 9999; background-color: var(--semi-color-bg-0); backdrop-filter: saturate(180%) blur(30px); transition: all 0.2s; .top { display: flex; justify-content: space-between; align-items: center; } .heading { font-size: 16px; font-weight: 600; display: flex; align-items: center; .heading-title { margin-left: 5px; } } .trending-type { display: flex; .trending-type-button:first-child { border-top-right-radius: 0; border-bottom-right-radius: 0; } .trending-type-button:last-child { border-top-left-radius: 0; border-bottom-left-radius: 0; } } .settings { display: flex; align-items: center; justify-content: space-between; padding: 5px 0 3px 0; .logo { display: flex; align-items: center; img { width: 16px; height: 16px; margin-right: 4px; } } .top { display: flex; } } } ================================================ FILE: src/components/repository-content/index.js ================================================ import React from 'react' import {Typography, Layout, Card, Space, Tooltip} from '@douyinfe/semi-ui' import {IconBranch, IconSourceControl, IconStar} from '@douyinfe/semi-icons' import {URL} from '@/config' import {numberWithCommas, polyfill} from '@/utils' import {SkeletonPlaceholder} from '@/components' import styles from './styles.scss' const {Content} = Layout const {Text} = Typography const {open} = polyfill const RepositoryContent = ({list, loading}) => { return ( {Array.from({length: 5}).map((_, index) => ( ))} {list.map(item => { return (
open(`${URL.GITHUB}/${item.author}`)}> {item.author} {' / '} open(item.url)}> {item.name}
} className={styles.repo} headerExtraContent={ {item.language || 'Unknown'} } > {item.description ? ( {item.description} ) : null}
open(`${item.url}/stargazers`)}> {numberWithCommas(item.stars)} open(`${item.url}/network/members.${item.author}`)}> {numberWithCommas(item.forks)} {numberWithCommas(item.currentPeriodStars)} stars today
{item.builtBy?.length ? (
Built by
{item.builtBy?.map(builtByAuthor => { return ( open(builtByAuthor.href)} /> ) })}
) : null}
) })}
) } export default RepositoryContent ================================================ FILE: src/components/repository-content/styles.scss ================================================ .content { padding-top: 15px; .repo-header { display: flex; align-items: center; } .repo-author { margin-left: 5px; width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .repo { width: 100%; margin-bottom: 20px; background-color: var(--semi-color-fill-0); } .language-color { display: inline-block; width: 10px; height: 10px; border-radius: 50%; } .description { display: block; margin-bottom: 20px; } .bottom-area { display: flex; flex-direction: column; .top { display: flex; justify-content: space-between; align-items: center; } .avatar { width: 20px; height: 20px; border-radius: 50%; margin-right: 4px; cursor: pointer; &:last-child { margin-right: 0; } } .bottom { display: flex; align-items: center; margin-top: 10px; } } .cursor { cursor: pointer; } } ================================================ FILE: src/components/settings-modal/index.js ================================================ import React from 'react' import {Button, Divider, Modal, Space, Switch, Typography} from '@douyinfe/semi-ui' import {IconExternalOpen} from '@douyinfe/semi-icons' import {useAutoUpdate, useBackTop, useDockIcon, useMode} from '@/hooks' import {MODE, URL, VERSION, Z_INDEX, isMac, isElectron} from '@/config' import {polyfill} from '@/utils' import pkg from '@pkg' import styles from './styles.scss' const {Text} = Typography const {open} = polyfill const SettingsModal = ({visible, setVisible}) => { const [mode, setMode] = useMode() const [backTop, setBackTop] = useBackTop() const [dockIcon, setDockIcon] = useDockIcon() const [autoUpdate, setAutoUpdate] = useAutoUpdate() return ( setVisible(false)} onCancel={() => setVisible(false)} closeOnEsc={true} width={350} height="fit-content" centered footer={null} zIndex={Z_INDEX.MODAL} >
Dark mode { setMode(e ? MODE.DARK : MODE.LIGHT) }} />
Show back to top button
{isMac && isElectron ? (
Show app icon in dock
) : null} {isElectron ? (
Automatic updates
) : null}
Changelog
Experiencing a bug?
{pkg.productName}, version {VERSION}
) } export default SettingsModal ================================================ FILE: src/components/settings-modal/styles.scss ================================================ .settings-modal { padding: 10px 0 20px; box-sizing: border-box; height: 100%; display: flex; flex-direction: column; justify-content: space-between; } .settings-item { width: 100%; display: flex; align-items: center; justify-content: space-between; } .version { margin-bottom: 4px; } ================================================ FILE: src/components/skeleton-placeholder/index.js ================================================ import React from 'react' import {Skeleton} from '@douyinfe/semi-ui' import styles from './styles.scss' const SkeletonPlaceholder = ({loading}) => { return ( } loading={loading} active /> ) } export default SkeletonPlaceholder ================================================ FILE: src/components/skeleton-placeholder/styles.scss ================================================ .skeleton { width: 100%; height: 200; border-radius: 6; margin-bottom: 20; } ================================================ FILE: src/components/update-notification/index.js ================================================ import React, {useEffect, useState} from 'react' import {Banner, Button} from '@douyinfe/semi-ui' import {IPC_FUNCTION} from '@shared' import {polyfill} from '@/utils' import styles from './styles.scss' const {send, receive} = polyfill const {QUIT_AND_INSTALL, SHOW_UPDATE_NOTIFICATION} = IPC_FUNCTION const UpdateNotification = () => { const [visible, setVisible] = useState(false) useEffect(() => { receive(SHOW_UPDATE_NOTIFICATION, () => setVisible(true)) }, []) if (!visible) return null return (
) } export default UpdateNotification ================================================ FILE: src/components/update-notification/styles.scss ================================================ .update { position: fixed; bottom: 0; left: 0; z-index: 99999; width: 100vw; background-color: var(--semi-color-bg-0); } .update-banner { padding: 20px; } .update-btns { display: flex; justify-content: flex-end; } .btn-confirm { margin-left: 10px; } ================================================ FILE: src/components/upper-container/index.js ================================================ import React, {useLayoutEffect, useState} from 'react' const UpperContainer = ({children}) => { const [footerHeight, setFooterHeight] = useState(0) useLayoutEffect(() => { const footerComponent = document.getElementById('footer') setFooterHeight(footerComponent.offsetHeight) }, []) return
{children}
} export default UpperContainer ================================================ FILE: src/config/constants.js ================================================ import pkg from '@pkg' import {isElectron as checkIsElectron} from '@/lib' export const VERSION = pkg.version export const TRENDING_TYPE = { REPOSITORIES: 'Repositories', DEVELOPERS: 'Developers', } export const MODE = { LIGHT: 'light', DARK: 'dark', } export const SINCE = { DAILY: 'daily', WEEKLY: 'weekly', MONTHLY: 'monthly', } export const SINCE_MAP = { [SINCE.DAILY]: 'Today', [SINCE.WEEKLY]: 'This week', [SINCE.MONTHLY]: 'This month', } export const SINCE_ARRAY = [ {name: SINCE_MAP[SINCE.DAILY], value: SINCE.DAILY}, {name: SINCE_MAP[SINCE.WEEKLY], value: SINCE.WEEKLY}, {name: SINCE_MAP[SINCE.MONTHLY], value: SINCE.MONTHLY}, ] export const Z_INDEX = { MODAL: 99999, TOAST: 99999, SELECT: 9999, } export const URL = { GITHUB: 'https://github.com', CHANGELOG: 'https://github.com/meetyan/raise/releases', ISSUE: 'https://github.com/meetyan/raise/issues', SERVER: 'https://trending.curve.to', } export const ALLOWED_TIME_OF_INACTIVITY = 1000 * 60 * 60 * 3 // 3 hours export const isElectron = checkIsElectron() export const isMac = window.navigator?.userAgentData?.platform.toUpperCase().includes('MAC') export const isChrome = !!process.env.isChrome export const isDev = !!process.env.WEBPACK_DEV ================================================ FILE: src/config/index.js ================================================ export {default as LANGUAGES} from './languages' export {default as SPOKEN_LANGUAGES} from './spoken-languages' export * from './constants' ================================================ FILE: src/config/languages.js ================================================ export default [ {urlParam: 'any', name: 'Any'}, {urlParam: '1c-enterprise', name: '1C Enterprise'}, {urlParam: 'abap', name: 'ABAP'}, {urlParam: 'abnf', name: 'ABNF'}, {urlParam: 'actionscript', name: 'ActionScript'}, {urlParam: 'ada', name: 'Ada'}, {urlParam: 'adobe-font-metrics', name: 'Adobe Font Metrics'}, {urlParam: 'agda', name: 'Agda'}, {urlParam: 'ags-script', name: 'AGS Script'}, {urlParam: 'alloy', name: 'Alloy'}, {urlParam: 'alpine-abuild', name: 'Alpine Abuild'}, {urlParam: 'ampl', name: 'AMPL'}, {urlParam: 'angelscript', name: 'AngelScript'}, {urlParam: 'ant-build-system', name: 'Ant Build System'}, {urlParam: 'antlr', name: 'ANTLR'}, {urlParam: 'apacheconf', name: 'ApacheConf'}, {urlParam: 'apex', name: 'Apex'}, {urlParam: 'api-blueprint', name: 'API Blueprint'}, {urlParam: 'apl', name: 'APL'}, {urlParam: 'apollo-guidance-computer', name: 'Apollo Guidance Computer'}, {urlParam: 'applescript', name: 'AppleScript'}, {urlParam: 'arc', name: 'Arc'}, {urlParam: 'asciidoc', name: 'AsciiDoc'}, {urlParam: 'asn.1', name: 'ASN.1'}, {urlParam: 'asp', name: 'ASP'}, {urlParam: 'aspectj', name: 'AspectJ'}, {urlParam: 'assembly', name: 'Assembly'}, {urlParam: 'ats', name: 'ATS'}, {urlParam: 'augeas', name: 'Augeas'}, {urlParam: 'autohotkey', name: 'AutoHotkey'}, {urlParam: 'autoit', name: 'AutoIt'}, {urlParam: 'awk', name: 'Awk'}, {urlParam: 'ballerina', name: 'Ballerina'}, {urlParam: 'batchfile', name: 'Batchfile'}, {urlParam: 'befunge', name: 'Befunge'}, {urlParam: 'bison', name: 'Bison'}, {urlParam: 'bitbake', name: 'BitBake'}, {urlParam: 'blade', name: 'Blade'}, {urlParam: 'blitzbasic', name: 'BlitzBasic'}, {urlParam: 'blitzmax', name: 'BlitzMax'}, {urlParam: 'bluespec', name: 'Bluespec'}, {urlParam: 'boo', name: 'Boo'}, {urlParam: 'brainfuck', name: 'Brainfuck'}, {urlParam: 'brightscript', name: 'Brightscript'}, {urlParam: 'bro', name: 'Bro'}, {urlParam: 'c', name: 'C'}, {urlParam: 'c%23', name: 'C#'}, {urlParam: 'c%2B%2B', name: 'C++'}, {urlParam: 'c-objdump', name: 'C-ObjDump'}, {urlParam: 'c2hs-haskell', name: 'C2hs Haskell'}, {urlParam: "cap'n-proto", name: "Cap'n Proto"}, {urlParam: 'cartocss', name: 'CartoCSS'}, {urlParam: 'ceylon', name: 'Ceylon'}, {urlParam: 'chapel', name: 'Chapel'}, {urlParam: 'charity', name: 'Charity'}, {urlParam: 'chuck', name: 'ChucK'}, {urlParam: 'cirru', name: 'Cirru'}, {urlParam: 'clarion', name: 'Clarion'}, {urlParam: 'clean', name: 'Clean'}, {urlParam: 'click', name: 'Click'}, {urlParam: 'clips', name: 'CLIPS'}, {urlParam: 'clojure', name: 'Clojure'}, {urlParam: 'closure-templates', name: 'Closure Templates'}, {urlParam: 'cmake', name: 'CMake'}, {urlParam: 'cobol', name: 'COBOL'}, {urlParam: 'coffeescript', name: 'CoffeeScript'}, {urlParam: 'coldfusion', name: 'ColdFusion'}, {urlParam: 'coldfusion-cfc', name: 'ColdFusion CFC'}, {urlParam: 'collada', name: 'COLLADA'}, {urlParam: 'common-lisp', name: 'Common Lisp'}, {urlParam: 'common-workflow-language', name: 'Common Workflow Language'}, {urlParam: 'component-pascal', name: 'Component Pascal'}, {urlParam: 'cool', name: 'Cool'}, {urlParam: 'coq', name: 'Coq'}, {urlParam: 'cpp-objdump', name: 'Cpp-ObjDump'}, {urlParam: 'creole', name: 'Creole'}, {urlParam: 'crystal', name: 'Crystal'}, {urlParam: 'cson', name: 'CSON'}, {urlParam: 'csound', name: 'Csound'}, {urlParam: 'csound-document', name: 'Csound Document'}, {urlParam: 'csound-score', name: 'Csound Score'}, {urlParam: 'css', name: 'CSS'}, {urlParam: 'csv', name: 'CSV'}, {urlParam: 'cuda', name: 'Cuda'}, {urlParam: 'cweb', name: 'CWeb'}, {urlParam: 'cycript', name: 'Cycript'}, {urlParam: 'cython', name: 'Cython'}, {urlParam: 'd', name: 'D'}, {urlParam: 'd-objdump', name: 'D-ObjDump'}, {urlParam: 'darcs-patch', name: 'Darcs Patch'}, {urlParam: 'dart', name: 'Dart'}, {urlParam: 'dataweave', name: 'DataWeave'}, {urlParam: 'desktop', name: 'desktop'}, {urlParam: 'diff', name: 'Diff'}, {urlParam: 'digital-command-language', name: 'DIGITAL Command Language'}, {urlParam: 'dm', name: 'DM'}, {urlParam: 'dns-zone', name: 'DNS Zone'}, {urlParam: 'dockerfile', name: 'Dockerfile'}, {urlParam: 'dogescript', name: 'Dogescript'}, {urlParam: 'dtrace', name: 'DTrace'}, {urlParam: 'dylan', name: 'Dylan'}, {urlParam: 'e', name: 'E'}, {urlParam: 'eagle', name: 'Eagle'}, {urlParam: 'easybuild', name: 'Easybuild'}, {urlParam: 'ebnf', name: 'EBNF'}, {urlParam: 'ec', name: 'eC'}, {urlParam: 'ecere-projects', name: 'Ecere Projects'}, {urlParam: 'ecl', name: 'ECL'}, {urlParam: 'eclipse', name: 'ECLiPSe'}, {urlParam: 'edje-data-collection', name: 'Edje Data Collection'}, {urlParam: 'edn', name: 'edn'}, {urlParam: 'eiffel', name: 'Eiffel'}, {urlParam: 'ejs', name: 'EJS'}, {urlParam: 'elixir', name: 'Elixir'}, {urlParam: 'elm', name: 'Elm'}, {urlParam: 'emacs-lisp', name: 'Emacs Lisp'}, {urlParam: 'emberscript', name: 'EmberScript'}, {urlParam: 'eq', name: 'EQ'}, {urlParam: 'erlang', name: 'Erlang'}, {urlParam: 'f%23', name: 'F#'}, {urlParam: 'factor', name: 'Factor'}, {urlParam: 'fancy', name: 'Fancy'}, {urlParam: 'fantom', name: 'Fantom'}, {urlParam: 'filebench-wml', name: 'Filebench WML'}, {urlParam: 'filterscript', name: 'Filterscript'}, {urlParam: 'fish', name: 'fish'}, {urlParam: 'flux', name: 'FLUX'}, {urlParam: 'formatted', name: 'Formatted'}, {urlParam: 'forth', name: 'Forth'}, {urlParam: 'fortran', name: 'Fortran'}, {urlParam: 'freemarker', name: 'FreeMarker'}, {urlParam: 'frege', name: 'Frege'}, {urlParam: 'g-code', name: 'G-code'}, {urlParam: 'game-maker-language', name: 'Game Maker Language'}, {urlParam: 'gams', name: 'GAMS'}, {urlParam: 'gap', name: 'GAP'}, {urlParam: 'gcc-machine-description', name: 'GCC Machine Description'}, {urlParam: 'gdb', name: 'GDB'}, {urlParam: 'gdscript', name: 'GDScript'}, {urlParam: 'genie', name: 'Genie'}, {urlParam: 'genshi', name: 'Genshi'}, {urlParam: 'gentoo-ebuild', name: 'Gentoo Ebuild'}, {urlParam: 'gentoo-eclass', name: 'Gentoo Eclass'}, {urlParam: 'gerber-image', name: 'Gerber Image'}, {urlParam: 'gettext-catalog', name: 'Gettext Catalog'}, {urlParam: 'gherkin', name: 'Gherkin'}, {urlParam: 'glsl', name: 'GLSL'}, {urlParam: 'glyph', name: 'Glyph'}, {urlParam: 'gn', name: 'GN'}, {urlParam: 'gnuplot', name: 'Gnuplot'}, {urlParam: 'go', name: 'Go'}, {urlParam: 'golo', name: 'Golo'}, {urlParam: 'gosu', name: 'Gosu'}, {urlParam: 'grace', name: 'Grace'}, {urlParam: 'gradle', name: 'Gradle'}, {urlParam: 'grammatical-framework', name: 'Grammatical Framework'}, {urlParam: 'graph-modeling-language', name: 'Graph Modeling Language'}, {urlParam: 'graphql', name: 'GraphQL'}, {urlParam: 'graphviz-(dot)', name: 'Graphviz (DOT)'}, {urlParam: 'groovy', name: 'Groovy'}, {urlParam: 'groovy-server-pages', name: 'Groovy Server Pages'}, {urlParam: 'hack', name: 'Hack'}, {urlParam: 'haml', name: 'Haml'}, {urlParam: 'handlebars', name: 'Handlebars'}, {urlParam: 'harbour', name: 'Harbour'}, {urlParam: 'haskell', name: 'Haskell'}, {urlParam: 'haxe', name: 'Haxe'}, {urlParam: 'hcl', name: 'HCL'}, {urlParam: 'hlsl', name: 'HLSL'}, {urlParam: 'html', name: 'HTML'}, {urlParam: 'html%2Bdjango', name: 'HTML+Django'}, {urlParam: 'html%2Becr', name: 'HTML+ECR'}, {urlParam: 'html%2Beex', name: 'HTML+EEX'}, {urlParam: 'html%2Berb', name: 'HTML+ERB'}, {urlParam: 'html%2Bphp', name: 'HTML+PHP'}, {urlParam: 'http', name: 'HTTP'}, {urlParam: 'hy', name: 'Hy'}, {urlParam: 'hyphy', name: 'HyPhy'}, {urlParam: 'idl', name: 'IDL'}, {urlParam: 'idris', name: 'Idris'}, {urlParam: 'igor-pro', name: 'IGOR Pro'}, {urlParam: 'inform-7', name: 'Inform 7'}, {urlParam: 'ini', name: 'INI'}, {urlParam: 'inno-setup', name: 'Inno Setup'}, {urlParam: 'io', name: 'Io'}, {urlParam: 'ioke', name: 'Ioke'}, {urlParam: 'irc-log', name: 'IRC log'}, {urlParam: 'isabelle', name: 'Isabelle'}, {urlParam: 'isabelle-root', name: 'Isabelle ROOT'}, {urlParam: 'j', name: 'J'}, {urlParam: 'jasmin', name: 'Jasmin'}, {urlParam: 'java', name: 'Java'}, {urlParam: 'java-server-pages', name: 'Java Server Pages'}, {urlParam: 'javascript', name: 'JavaScript'}, {urlParam: 'jflex', name: 'JFlex'}, {urlParam: 'jison', name: 'Jison'}, {urlParam: 'jison-lex', name: 'Jison Lex'}, {urlParam: 'jolie', name: 'Jolie'}, {urlParam: 'json', name: 'JSON'}, {urlParam: 'json5', name: 'JSON5'}, {urlParam: 'jsoniq', name: 'JSONiq'}, {urlParam: 'jsonld', name: 'JSONLD'}, {urlParam: 'jsx', name: 'JSX'}, {urlParam: 'julia', name: 'Julia'}, {urlParam: 'jupyter-notebook', name: 'Jupyter Notebook'}, {urlParam: 'kicad-layout', name: 'KiCad Layout'}, {urlParam: 'kicad-legacy-layout', name: 'KiCad Legacy Layout'}, {urlParam: 'kicad-schematic', name: 'KiCad Schematic'}, {urlParam: 'kit', name: 'Kit'}, {urlParam: 'kotlin', name: 'Kotlin'}, {urlParam: 'krl', name: 'KRL'}, {urlParam: 'labview', name: 'LabVIEW'}, {urlParam: 'lasso', name: 'Lasso'}, {urlParam: 'latte', name: 'Latte'}, {urlParam: 'lean', name: 'Lean'}, {urlParam: 'less', name: 'Less'}, {urlParam: 'lex', name: 'Lex'}, {urlParam: 'lfe', name: 'LFE'}, {urlParam: 'lilypond', name: 'LilyPond'}, {urlParam: 'limbo', name: 'Limbo'}, {urlParam: 'linker-script', name: 'Linker Script'}, {urlParam: 'linux-kernel-module', name: 'Linux Kernel Module'}, {urlParam: 'liquid', name: 'Liquid'}, {urlParam: 'literate-agda', name: 'Literate Agda'}, {urlParam: 'literate-coffeescript', name: 'Literate CoffeeScript'}, {urlParam: 'literate-haskell', name: 'Literate Haskell'}, {urlParam: 'livescript', name: 'LiveScript'}, {urlParam: 'llvm', name: 'LLVM'}, {urlParam: 'logos', name: 'Logos'}, {urlParam: 'logtalk', name: 'Logtalk'}, {urlParam: 'lolcode', name: 'LOLCODE'}, {urlParam: 'lookml', name: 'LookML'}, {urlParam: 'loomscript', name: 'LoomScript'}, {urlParam: 'lsl', name: 'LSL'}, {urlParam: 'lua', name: 'Lua'}, {urlParam: 'm', name: 'M'}, {urlParam: 'm4', name: 'M4'}, {urlParam: 'm4sugar', name: 'M4Sugar'}, {urlParam: 'makefile', name: 'Makefile'}, {urlParam: 'mako', name: 'Mako'}, {urlParam: 'markdown', name: 'Markdown'}, {urlParam: 'marko', name: 'Marko'}, {urlParam: 'mask', name: 'Mask'}, {urlParam: 'mathematica', name: 'Mathematica'}, {urlParam: 'matlab', name: 'Matlab'}, {urlParam: 'maven-pom', name: 'Maven POM'}, {urlParam: 'max', name: 'Max'}, {urlParam: 'maxscript', name: 'MAXScript'}, {urlParam: 'mediawiki', name: 'MediaWiki'}, {urlParam: 'mercury', name: 'Mercury'}, {urlParam: 'meson', name: 'Meson'}, {urlParam: 'metal', name: 'Metal'}, {urlParam: 'minid', name: 'MiniD'}, {urlParam: 'mirah', name: 'Mirah'}, {urlParam: 'modelica', name: 'Modelica'}, {urlParam: 'modula-2', name: 'Modula-2'}, {urlParam: 'module-management-system', name: 'Module Management System'}, {urlParam: 'monkey', name: 'Monkey'}, {urlParam: 'moocode', name: 'Moocode'}, {urlParam: 'moonscript', name: 'MoonScript'}, {urlParam: 'mql4', name: 'MQL4'}, {urlParam: 'mql5', name: 'MQL5'}, {urlParam: 'mtml', name: 'MTML'}, {urlParam: 'muf', name: 'MUF'}, {urlParam: 'mupad', name: 'mupad'}, {urlParam: 'myghty', name: 'Myghty'}, {urlParam: 'ncl', name: 'NCL'}, {urlParam: 'nearley', name: 'Nearley'}, {urlParam: 'nemerle', name: 'Nemerle'}, {urlParam: 'nesc', name: 'nesC'}, {urlParam: 'netlinx', name: 'NetLinx'}, {urlParam: 'netlinx%2Berb', name: 'NetLinx+ERB'}, {urlParam: 'netlogo', name: 'NetLogo'}, {urlParam: 'newlisp', name: 'NewLisp'}, {urlParam: 'nextflow', name: 'Nextflow'}, {urlParam: 'nginx', name: 'Nginx'}, {urlParam: 'nim', name: 'Nim'}, {urlParam: 'ninja', name: 'Ninja'}, {urlParam: 'nit', name: 'Nit'}, {urlParam: 'nix', name: 'Nix'}, {urlParam: 'nl', name: 'NL'}, {urlParam: 'nsis', name: 'NSIS'}, {urlParam: 'nu', name: 'Nu'}, {urlParam: 'numpy', name: 'NumPy'}, {urlParam: 'objdump', name: 'ObjDump'}, {urlParam: 'objective-c', name: 'Objective-C'}, {urlParam: 'objective-c%2B%2B', name: 'Objective-C++'}, {urlParam: 'objective-j', name: 'Objective-J'}, {urlParam: 'ocaml', name: 'OCaml'}, {urlParam: 'omgrofl', name: 'Omgrofl'}, {urlParam: 'ooc', name: 'ooc'}, {urlParam: 'opa', name: 'Opa'}, {urlParam: 'opal', name: 'Opal'}, {urlParam: 'opencl', name: 'OpenCL'}, {urlParam: 'openedge-abl', name: 'OpenEdge ABL'}, {urlParam: 'openrc-runscript', name: 'OpenRC runscript'}, {urlParam: 'openscad', name: 'OpenSCAD'}, {urlParam: 'opentype-feature-file', name: 'OpenType Feature File'}, {urlParam: 'org', name: 'Org'}, {urlParam: 'ox', name: 'Ox'}, {urlParam: 'oxygene', name: 'Oxygene'}, {urlParam: 'oz', name: 'Oz'}, {urlParam: 'p4', name: 'P4'}, {urlParam: 'pan', name: 'Pan'}, {urlParam: 'papyrus', name: 'Papyrus'}, {urlParam: 'parrot', name: 'Parrot'}, {urlParam: 'parrot-assembly', name: 'Parrot Assembly'}, {urlParam: 'parrot-internal-representation', name: 'Parrot Internal Representation'}, {urlParam: 'pascal', name: 'Pascal'}, {urlParam: 'pawn', name: 'PAWN'}, {urlParam: 'pep8', name: 'Pep8'}, {urlParam: 'perl', name: 'Perl'}, {urlParam: 'perl-6', name: 'Perl 6'}, {urlParam: 'php', name: 'PHP'}, {urlParam: 'pic', name: 'Pic'}, {urlParam: 'pickle', name: 'Pickle'}, {urlParam: 'picolisp', name: 'PicoLisp'}, {urlParam: 'piglatin', name: 'PigLatin'}, {urlParam: 'pike', name: 'Pike'}, {urlParam: 'plpgsql', name: 'PLpgSQL'}, {urlParam: 'plsql', name: 'PLSQL'}, {urlParam: 'pod', name: 'Pod'}, {urlParam: 'pogoscript', name: 'PogoScript'}, {urlParam: 'pony', name: 'Pony'}, {urlParam: 'postcss', name: 'PostCSS'}, {urlParam: 'postscript', name: 'PostScript'}, {urlParam: 'pov-ray-sdl', name: 'POV-Ray SDL'}, {urlParam: 'powerbuilder', name: 'PowerBuilder'}, {urlParam: 'powershell', name: 'PowerShell'}, {urlParam: 'processing', name: 'Processing'}, {urlParam: 'prolog', name: 'Prolog'}, {urlParam: 'propeller-spin', name: 'Propeller Spin'}, {urlParam: 'protocol-buffer', name: 'Protocol Buffer'}, {urlParam: 'public-key', name: 'Public Key'}, {urlParam: 'pug', name: 'Pug'}, {urlParam: 'puppet', name: 'Puppet'}, {urlParam: 'pure-data', name: 'Pure Data'}, {urlParam: 'purebasic', name: 'PureBasic'}, {urlParam: 'purescript', name: 'PureScript'}, {urlParam: 'python', name: 'Python'}, {urlParam: 'python-console', name: 'Python console'}, {urlParam: 'python-traceback', name: 'Python traceback'}, {urlParam: 'qmake', name: 'QMake'}, {urlParam: 'qml', name: 'QML'}, {urlParam: 'r', name: 'R'}, {urlParam: 'racket', name: 'Racket'}, {urlParam: 'ragel', name: 'Ragel'}, {urlParam: 'raml', name: 'RAML'}, {urlParam: 'rascal', name: 'Rascal'}, {urlParam: 'raw-token-data', name: 'Raw token data'}, {urlParam: 'rdoc', name: 'RDoc'}, {urlParam: 'realbasic', name: 'REALbasic'}, {urlParam: 'reason', name: 'Reason'}, {urlParam: 'rebol', name: 'Rebol'}, {urlParam: 'red', name: 'Red'}, {urlParam: 'redcode', name: 'Redcode'}, {urlParam: 'regular-expression', name: 'Regular Expression'}, {urlParam: "ren'py", name: "Ren'Py"}, {urlParam: 'renderscript', name: 'RenderScript'}, {urlParam: 'restructuredtext', name: 'reStructuredText'}, {urlParam: 'rexx', name: 'REXX'}, {urlParam: 'rhtml', name: 'RHTML'}, {urlParam: 'ring', name: 'Ring'}, {urlParam: 'rmarkdown', name: 'RMarkdown'}, {urlParam: 'robotframework', name: 'RobotFramework'}, {urlParam: 'roff', name: 'Roff'}, {urlParam: 'rouge', name: 'Rouge'}, {urlParam: 'rpc', name: 'RPC'}, {urlParam: 'rpm-spec', name: 'RPM Spec'}, {urlParam: 'ruby', name: 'Ruby'}, {urlParam: 'runoff', name: 'RUNOFF'}, {urlParam: 'rust', name: 'Rust'}, {urlParam: 'sage', name: 'Sage'}, {urlParam: 'saltstack', name: 'SaltStack'}, {urlParam: 'sas', name: 'SAS'}, {urlParam: 'sass', name: 'Sass'}, {urlParam: 'scala', name: 'Scala'}, {urlParam: 'scaml', name: 'Scaml'}, {urlParam: 'scheme', name: 'Scheme'}, {urlParam: 'scilab', name: 'Scilab'}, {urlParam: 'scss', name: 'SCSS'}, {urlParam: 'sed', name: 'sed'}, {urlParam: 'self', name: 'Self'}, {urlParam: 'shaderlab', name: 'ShaderLab'}, {urlParam: 'shell', name: 'Shell'}, {urlParam: 'shellsession', name: 'ShellSession'}, {urlParam: 'shen', name: 'Shen'}, {urlParam: 'slash', name: 'Slash'}, {urlParam: 'slim', name: 'Slim'}, {urlParam: 'smali', name: 'Smali'}, {urlParam: 'smalltalk', name: 'Smalltalk'}, {urlParam: 'smarty', name: 'Smarty'}, {urlParam: 'smt', name: 'SMT'}, {urlParam: 'solidity', name: 'Solidity'}, {urlParam: 'sourcepawn', name: 'SourcePawn'}, {urlParam: 'sparql', name: 'SPARQL'}, {urlParam: 'spline-font-database', name: 'Spline Font Database'}, {urlParam: 'sqf', name: 'SQF'}, {urlParam: 'sql', name: 'SQL'}, {urlParam: 'sqlpl', name: 'SQLPL'}, {urlParam: 'squirrel', name: 'Squirrel'}, {urlParam: 'srecode-template', name: 'SRecode Template'}, {urlParam: 'stan', name: 'Stan'}, {urlParam: 'standard-ml', name: 'Standard ML'}, {urlParam: 'stata', name: 'Stata'}, {urlParam: 'ston', name: 'STON'}, {urlParam: 'stylus', name: 'Stylus'}, {urlParam: 'sublime-text-config', name: 'Sublime Text Config'}, {urlParam: 'subrip-text', name: 'SubRip Text'}, {urlParam: 'sugarss', name: 'SugarSS'}, {urlParam: 'supercollider', name: 'SuperCollider'}, {urlParam: 'svg', name: 'SVG'}, {urlParam: 'swift', name: 'Swift'}, {urlParam: 'systemverilog', name: 'SystemVerilog'}, {urlParam: 'tcl', name: 'Tcl'}, {urlParam: 'tcsh', name: 'Tcsh'}, {urlParam: 'tea', name: 'Tea'}, {urlParam: 'terra', name: 'Terra'}, {urlParam: 'tex', name: 'TeX'}, {urlParam: 'text', name: 'Text'}, {urlParam: 'textile', name: 'Textile'}, {urlParam: 'thrift', name: 'Thrift'}, {urlParam: 'ti-program', name: 'TI Program'}, {urlParam: 'tla', name: 'TLA'}, {urlParam: 'toml', name: 'TOML'}, {urlParam: 'turing', name: 'Turing'}, {urlParam: 'turtle', name: 'Turtle'}, {urlParam: 'twig', name: 'Twig'}, {urlParam: 'txl', name: 'TXL'}, {urlParam: 'type-language', name: 'Type Language'}, {urlParam: 'typescript', name: 'TypeScript'}, {urlParam: 'unified-parallel-c', name: 'Unified Parallel C'}, {urlParam: 'unity3d-asset', name: 'Unity3D Asset'}, {urlParam: 'unix-assembly', name: 'Unix Assembly'}, {urlParam: 'uno', name: 'Uno'}, {urlParam: 'unrealscript', name: 'UnrealScript'}, {urlParam: 'urweb', name: 'UrWeb'}, {urlParam: 'vala', name: 'Vala'}, {urlParam: 'vcl', name: 'VCL'}, {urlParam: 'verilog', name: 'Verilog'}, {urlParam: 'vhdl', name: 'VHDL'}, {urlParam: 'vim-script', name: 'Vim script'}, {urlParam: 'visual-basic', name: 'Visual Basic'}, {urlParam: 'volt', name: 'Volt'}, {urlParam: 'vue', name: 'Vue'}, {urlParam: 'wavefront-material', name: 'Wavefront Material'}, {urlParam: 'wavefront-object', name: 'Wavefront Object'}, {urlParam: 'wdl', name: 'wdl'}, {urlParam: 'web-ontology-language', name: 'Web Ontology Language'}, {urlParam: 'webassembly', name: 'WebAssembly'}, {urlParam: 'webidl', name: 'WebIDL'}, {urlParam: 'wisp', name: 'wisp'}, {urlParam: 'world-of-warcraft-addon-data', name: 'World of Warcraft Addon Data'}, {urlParam: 'x10', name: 'X10'}, {urlParam: 'xbase', name: 'xBase'}, {urlParam: 'xc', name: 'XC'}, {urlParam: 'xcompose', name: 'XCompose'}, {urlParam: 'xml', name: 'XML'}, {urlParam: 'xojo', name: 'Xojo'}, {urlParam: 'xpages', name: 'XPages'}, {urlParam: 'xpm', name: 'XPM'}, {urlParam: 'xproc', name: 'XProc'}, {urlParam: 'xquery', name: 'XQuery'}, {urlParam: 'xs', name: 'XS'}, {urlParam: 'xslt', name: 'XSLT'}, {urlParam: 'xtend', name: 'Xtend'}, {urlParam: 'yacc', name: 'Yacc'}, {urlParam: 'yaml', name: 'YAML'}, {urlParam: 'yang', name: 'YANG'}, {urlParam: 'yara', name: 'YARA'}, {urlParam: 'zephir', name: 'Zephir'}, {urlParam: 'zimpl', name: 'Zimpl'}, ] ================================================ FILE: src/config/spoken-languages.js ================================================ export default [ {urlParam: 'any', name: 'Any'}, {urlParam: 'ab', name: 'Abkhazian'}, {urlParam: 'aa', name: 'Afar'}, {urlParam: 'af', name: 'Afrikaans'}, {urlParam: 'ak', name: 'Akan'}, {urlParam: 'sq', name: 'Albanian'}, {urlParam: 'am', name: 'Amharic'}, {urlParam: 'ar', name: 'Arabic'}, {urlParam: 'an', name: 'Aragonese'}, {urlParam: 'hy', name: 'Armenian'}, {urlParam: 'as', name: 'Assamese'}, {urlParam: 'av', name: 'Avaric'}, {urlParam: 'ae', name: 'Avestan'}, {urlParam: 'ay', name: 'Aymara'}, {urlParam: 'az', name: 'Azerbaijani'}, {urlParam: 'bm', name: 'Bambara'}, {urlParam: 'ba', name: 'Bashkir'}, {urlParam: 'eu', name: 'Basque'}, {urlParam: 'be', name: 'Belarusian'}, {urlParam: 'bn', name: 'Bengali'}, {urlParam: 'bh', name: 'Bihari languages'}, {urlParam: 'bi', name: 'Bislama'}, {urlParam: 'bs', name: 'Bosnian'}, {urlParam: 'br', name: 'Breton'}, {urlParam: 'bg', name: 'Bulgarian'}, {urlParam: 'my', name: 'Burmese'}, {urlParam: 'ca', name: 'Catalan, Valencian'}, {urlParam: 'ch', name: 'Chamorro'}, {urlParam: 'ce', name: 'Chechen'}, {urlParam: 'ny', name: 'Chichewa, Chewa, Nyanja'}, {urlParam: 'zh', name: 'Chinese'}, {urlParam: 'cv', name: 'Chuvash'}, {urlParam: 'kw', name: 'Cornish'}, {urlParam: 'co', name: 'Corsican'}, {urlParam: 'cr', name: 'Cree'}, {urlParam: 'hr', name: 'Croatian'}, {urlParam: 'cs', name: 'Czech'}, {urlParam: 'da', name: 'Danish'}, {urlParam: 'dv', name: 'Divehi, Dhivehi, Maldivian'}, {urlParam: 'nl', name: 'Dutch, Flemish'}, {urlParam: 'dz', name: 'Dzongkha'}, {urlParam: 'en', name: 'English'}, {urlParam: 'eo', name: 'Esperanto'}, {urlParam: 'et', name: 'Estonian'}, {urlParam: 'ee', name: 'Ewe'}, {urlParam: 'fo', name: 'Faroese'}, {urlParam: 'fj', name: 'Fijian'}, {urlParam: 'fi', name: 'Finnish'}, {urlParam: 'fr', name: 'French'}, {urlParam: 'ff', name: 'Fulah'}, {urlParam: 'gl', name: 'Galician'}, {urlParam: 'ka', name: 'Georgian'}, {urlParam: 'de', name: 'German'}, {urlParam: 'el', name: 'Greek, Modern'}, {urlParam: 'gn', name: 'Guarani'}, {urlParam: 'gu', name: 'Gujarati'}, {urlParam: 'ht', name: 'Haitian, Haitian Creole'}, {urlParam: 'ha', name: 'Hausa'}, {urlParam: 'he', name: 'Hebrew'}, {urlParam: 'hz', name: 'Herero'}, {urlParam: 'hi', name: 'Hindi'}, {urlParam: 'ho', name: 'Hiri Motu'}, {urlParam: 'hu', name: 'Hungarian'}, {urlParam: 'ia', name: 'Interlingua (International Auxil...'}, {urlParam: 'id', name: 'Indonesian'}, {urlParam: 'ie', name: 'Interlingue, Occidental'}, {urlParam: 'ga', name: 'Irish'}, {urlParam: 'ig', name: 'Igbo'}, {urlParam: 'ik', name: 'Inupiaq'}, {urlParam: 'io', name: 'Ido'}, {urlParam: 'is', name: 'Icelandic'}, {urlParam: 'it', name: 'Italian'}, {urlParam: 'iu', name: 'Inuktitut'}, {urlParam: 'ja', name: 'Japanese'}, {urlParam: 'jv', name: 'Javanese'}, {urlParam: 'kl', name: 'Kalaallisut, Greenlandic'}, {urlParam: 'kn', name: 'Kannada'}, {urlParam: 'kr', name: 'Kanuri'}, {urlParam: 'ks', name: 'Kashmiri'}, {urlParam: 'kk', name: 'Kazakh'}, {urlParam: 'km', name: 'Central Khmer'}, {urlParam: 'ki', name: 'Kikuyu, Gikuyu'}, {urlParam: 'rw', name: 'Kinyarwanda'}, {urlParam: 'ky', name: 'Kirghiz, Kyrgyz'}, {urlParam: 'kv', name: 'Komi'}, {urlParam: 'kg', name: 'Kongo'}, {urlParam: 'ko', name: 'Korean'}, {urlParam: 'ku', name: 'Kurdish'}, {urlParam: 'kj', name: 'Kuanyama, Kwanyama'}, {urlParam: 'la', name: 'Latin'}, {urlParam: 'lb', name: 'Luxembourgish, Letzeburgesch'}, {urlParam: 'lg', name: 'Ganda'}, {urlParam: 'li', name: 'Limburgan, Limburger, Limburgish'}, {urlParam: 'ln', name: 'Lingala'}, {urlParam: 'lo', name: 'Lao'}, {urlParam: 'lt', name: 'Lithuanian'}, {urlParam: 'lu', name: 'Luba-Katanga'}, {urlParam: 'lv', name: 'Latvian'}, {urlParam: 'gv', name: 'Manx'}, {urlParam: 'mk', name: 'Macedonian'}, {urlParam: 'mg', name: 'Malagasy'}, {urlParam: 'ms', name: 'Malay'}, {urlParam: 'ml', name: 'Malayalam'}, {urlParam: 'mt', name: 'Maltese'}, {urlParam: 'mi', name: 'Maori'}, {urlParam: 'mr', name: 'Marathi'}, {urlParam: 'mh', name: 'Marshallese'}, {urlParam: 'mn', name: 'Mongolian'}, {urlParam: 'na', name: 'Nauru'}, {urlParam: 'nv', name: 'Navajo, Navaho'}, {urlParam: 'nd', name: 'North Ndebele'}, {urlParam: 'ne', name: 'Nepali'}, {urlParam: 'ng', name: 'Ndonga'}, {urlParam: 'nb', name: 'Norwegian Bokmål'}, {urlParam: 'nn', name: 'Norwegian Nynorsk'}, {urlParam: 'no', name: 'Norwegian'}, {urlParam: 'ii', name: 'Sichuan Yi, Nuosu'}, {urlParam: 'nr', name: 'South Ndebele'}, {urlParam: 'oc', name: 'Occitan'}, {urlParam: 'oj', name: 'Ojibwa'}, {urlParam: 'cu', name: 'Church Slavic, Old Slavonic, Chu...'}, {urlParam: 'om', name: 'Oromo'}, {urlParam: 'or', name: 'Oriya'}, {urlParam: 'os', name: 'Ossetian, Ossetic'}, {urlParam: 'pa', name: 'Punjabi, Panjabi'}, {urlParam: 'pi', name: 'Pali'}, {urlParam: 'fa', name: 'Persian'}, {urlParam: 'pl', name: 'Polish'}, {urlParam: 'ps', name: 'Pashto, Pushto'}, {urlParam: 'pt', name: 'Portuguese'}, {urlParam: 'qu', name: 'Quechua'}, {urlParam: 'rm', name: 'Romansh'}, {urlParam: 'rn', name: 'Rundi'}, {urlParam: 'ro', name: 'Romanian, Moldavian, Moldovan'}, {urlParam: 'ru', name: 'Russian'}, {urlParam: 'sa', name: 'Sanskrit'}, {urlParam: 'sc', name: 'Sardinian'}, {urlParam: 'sd', name: 'Sindhi'}, {urlParam: 'se', name: 'Northern Sami'}, {urlParam: 'sm', name: 'Samoan'}, {urlParam: 'sg', name: 'Sango'}, {urlParam: 'sr', name: 'Serbian'}, {urlParam: 'gd', name: 'Gaelic, Scottish Gaelic'}, {urlParam: 'sn', name: 'Shona'}, {urlParam: 'si', name: 'Sinhala, Sinhalese'}, {urlParam: 'sk', name: 'Slovak'}, {urlParam: 'sl', name: 'Slovenian'}, {urlParam: 'so', name: 'Somali'}, {urlParam: 'st', name: 'Southern Sotho'}, {urlParam: 'es', name: 'Spanish, Castilian'}, {urlParam: 'su', name: 'Sundanese'}, {urlParam: 'sw', name: 'Swahili'}, {urlParam: 'ss', name: 'Swati'}, {urlParam: 'sv', name: 'Swedish'}, {urlParam: 'ta', name: 'Tamil'}, {urlParam: 'te', name: 'Telugu'}, {urlParam: 'tg', name: 'Tajik'}, {urlParam: 'th', name: 'Thai'}, {urlParam: 'ti', name: 'Tigrinya'}, {urlParam: 'bo', name: 'Tibetan'}, {urlParam: 'tk', name: 'Turkmen'}, {urlParam: 'tl', name: 'Tagalog'}, {urlParam: 'tn', name: 'Tswana'}, {urlParam: 'to', name: 'Tonga (Tonga Islands)'}, {urlParam: 'tr', name: 'Turkish'}, {urlParam: 'ts', name: 'Tsonga'}, {urlParam: 'tt', name: 'Tatar'}, {urlParam: 'tw', name: 'Twi'}, {urlParam: 'ty', name: 'Tahitian'}, {urlParam: 'ug', name: 'Uighur, Uyghur'}, {urlParam: 'uk', name: 'Ukrainian'}, {urlParam: 'ur', name: 'Urdu'}, {urlParam: 'uz', name: 'Uzbek'}, {urlParam: 've', name: 'Venda'}, {urlParam: 'vi', name: 'Vietnamese'}, {urlParam: 'vo', name: 'Volapük'}, {urlParam: 'wa', name: 'Walloon'}, {urlParam: 'cy', name: 'Welsh'}, {urlParam: 'wo', name: 'Wolof'}, {urlParam: 'fy', name: 'Western Frisian'}, {urlParam: 'xh', name: 'Xhosa'}, {urlParam: 'yi', name: 'Yiddish'}, {urlParam: 'yo', name: 'Yoruba'}, {urlParam: 'za', name: 'Zhuang, Chuang'}, {urlParam: 'zu', name: 'Zulu'}, ] ================================================ FILE: src/hooks/index.js ================================================ export {default as useMode} from './use-mode' export {default as useDockIcon} from './use-dock-icon' export {default as useOutsideClick} from './use-outside-click' export {default as useScrollPosition} from './use-scroll-position' export * from './use-context-props' ================================================ FILE: src/hooks/use-context-props.js ================================================ import {useContextProp} from '@/app-context' import {STORAGE_KEY} from '@shared' export const useTrendingType = () => { return useContextProp(STORAGE_KEY.TRENDING_TYPE) } export const useBackTop = () => { return useContextProp(STORAGE_KEY.SHOW_BACK_TOP) } export const useAutoUpdate = () => { return useContextProp(STORAGE_KEY.ENABLE_AUTO_UPDATE) } ================================================ FILE: src/hooks/use-dock-icon.js ================================================ import {useContextProp} from '@/app-context' import {polyfill} from '@/utils' import {IPC_FUNCTION, STORAGE_KEY} from '@shared' const useDockIcon = () => { const [dockIcon, setDockIcon] = useContextProp(STORAGE_KEY.SHOW_DOCK_ICON) const _setDockIcon = visible => { polyfill.send(IPC_FUNCTION.SHOW_DOCK_ICON, visible) setDockIcon(visible) } return [dockIcon, _setDockIcon] } export default useDockIcon ================================================ FILE: src/hooks/use-mode.js ================================================ import {useContextProp} from '@/app-context' import {MODE} from '@/config' import {STORAGE_KEY} from '@shared' const useMode = () => { const [mode, setMode] = useContextProp(STORAGE_KEY.MODE) const _setMode = target => { const body = document.body if (target === MODE.LIGHT) { body.removeAttribute('theme-mode') setMode(MODE.LIGHT) } else { body.setAttribute('theme-mode', 'dark') setMode(MODE.DARK) } } return [mode, _setMode] } export default useMode ================================================ FILE: src/hooks/use-outside-click.js ================================================ import {useEffect} from 'react' /** * Executes a handler function on click outside of a specific div * See: https://stackoverflow.com/questions/32553158/detect-click-outside-react-component * @param {*} ref * @param {*} handler */ const useOutsideClick = (ref, handler) => { useEffect(() => { /** * Alert if clicked on outside of element */ function handleClickOutside(event) { if (ref.current && !ref.current.contains(event.target)) { handler() } } // Bind the event listener document.addEventListener('mousedown', handleClickOutside) return () => { // Unbind the event listener on clean up document.removeEventListener('mousedown', handleClickOutside) } }, [ref]) } export default useOutsideClick ================================================ FILE: src/hooks/use-scroll-position.js ================================================ import {useScroll} from 'ahooks' const useScrollPosition = () => { const scrollRef = useScroll() const scrollPosition = scrollRef?.top return scrollPosition } export default useScrollPosition ================================================ FILE: src/index.ejs ================================================ Raise - GitHub Trending
<% if (htmlWebpackPlugin.options.isChrome) { %> <% } %> <% if (htmlWebpackPlugin.options.isElectron) { %> <% } %> ================================================ FILE: src/index.js ================================================ import React from 'react' import {createRoot} from 'react-dom/client' import App from './app' const container = document.querySelector('#root') const root = createRoot(container) root.render() ================================================ FILE: src/io/index.js ================================================ export * from './trending' ================================================ FILE: src/io/interceptor.js ================================================ import axios from 'axios' import NProgress from 'nprogress' NProgress.configure({showSpinner: false}) axios.interceptors.request.use( config => { NProgress.start() return config }, error => { NProgress.start() return Promise.reject(error) } ) axios.interceptors.response.use( response => { NProgress.done() return response }, error => { NProgress.done() return Promise.reject(error) } ) export default axios ================================================ FILE: src/io/trending.js ================================================ import {snakeCase} from 'lodash' import axios from './interceptor' import {TRENDING_TYPE, URL} from '@/config' import {getTimeStamp} from '@/utils' let controller export let lastTimestamp = 0 const buildUrl = (baseUrl, params = {}) => { const queryString = Object.keys(params) .filter(key => params[key]) .map(key => `${snakeCase(key)}=${params[key]}`) .join('&') return queryString === '' ? baseUrl : `${baseUrl}?${queryString}` } const checkResponse = res => { if (res.status !== 200) { throw new Error('Something went wrong') } } const fetch = async ({params = {}, type, serverUrl = URL.SERVER} = {}) => { if (controller) { controller.abort() // Makes sure that users always get the latest result } controller = new AbortController() /** * Used to compare between now and inactivity. * Reloads if time of inactivity is too long */ lastTimestamp = getTimeStamp() const res = await axios({ method: 'get', url: buildUrl(`${serverUrl}/${type}`, params), signal: controller.signal, }) checkResponse(res) return res.data } export const fetchRepositories = (params, serverUrl = URL.SERVER) => { return fetch({params, serverUrl, type: TRENDING_TYPE.REPOSITORIES.toLocaleLowerCase()}) } export const fetchDevelopers = async (params, serverUrl = URL.SERVER) => { return fetch({params, serverUrl, type: TRENDING_TYPE.DEVELOPERS.toLocaleLowerCase()}) } ================================================ FILE: src/lib/index.js ================================================ export {default as isElectron} from './is-electron' ================================================ FILE: src/lib/is-electron.js ================================================ const isElectron = () => { // Renderer process if ( typeof window !== 'undefined' && typeof window.process === 'object' && window.process.type === 'renderer' ) { return true } // Main process if ( typeof process !== 'undefined' && typeof process.versions === 'object' && !!process.versions.electron ) { return true } // Detect the user agent when the `nodeIntegration` option is set to false if ( typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0 ) { return true } return false } export default isElectron ================================================ FILE: src/pages/index/index.js ================================================ import React, {useEffect, useState} from 'react' import {Typography, BackTop, Toast, Empty} from '@douyinfe/semi-ui' import {IconArrowUp} from '@douyinfe/semi-icons' import {IllustrationNoResult, IllustrationNoResultDark} from '@douyinfe/semi-illustrations' import axios from 'axios' import {RaiseHeader, RepositoryContent, DeveloperContent} from '@/components' import {fetchRepositories, fetchDevelopers, lastTimestamp} from '@/io' import {convert, polyfill} from '@/utils' import {ALLOWED_TIME_OF_INACTIVITY, TRENDING_TYPE} from '@/config' import {useBackTop, useMode, useTrendingType} from '@/hooks' import {IPC_FUNCTION} from '@shared' import styles from './styles.scss' const {Text} = Typography const {REPOSITORIES} = TRENDING_TYPE const {RELOAD_AFTER_INACTIVITY} = IPC_FUNCTION const Index = () => { const [trendingType] = useTrendingType() const [backTop] = useBackTop() const [mode, setMode] = useMode() const [list, setList] = useState([]) const [getListParams, setGetListParams] = useState({}) const [loading, setLoading] = useState(false) const [empty, setEmpty] = useState(false) const isRepo = trendingType === REPOSITORIES const Content = isRepo ? RepositoryContent : DeveloperContent const resetList = () => setList([]) const getList = async params => { window.scrollTo({top: 0}) setLoading(true) resetList() setGetListParams(params) setEmpty(false) let isCancel = false try { const fetch = isRepo ? fetchRepositories : fetchDevelopers const res = await fetch(convert(params)) setList(res) setEmpty(!res.length) } catch (error) { // Makes sure when a request is canceled, loading is still true for the next getList call if (axios.isCancel(error)) return (isCancel = true) console.log('An error occurred when calling getList. Params: ', params, error) Toast.error( 'Oops. It looks like an error occurs. The server might be down. Please try again.' ) } finally { setLoading(isCancel) } } const refresh = () => { getList(getListParams) } useEffect(() => { getList() }, [trendingType]) /** * Recover settings to last state according to context storage * e.g., when a user toggles settings in the settings modal, * a few changes have been made. * After he closes the app and reopens it, * all settings/context will have to be recovered. */ useEffect(() => { setMode(mode) }, []) useEffect(() => { const {receive} = polyfill const removeReloadListener = receive(RELOAD_AFTER_INACTIVITY, () => { const now = new Date().getTime() if (lastTimestamp && now - lastTimestamp > ALLOWED_TIME_OF_INACTIVITY) { getList(getListParams) } }) return () => { removeReloadListener() } }, [getListParams]) return ( <> {empty ? ( } darkModeImage={} description={ {`It looks like we don’t have any trending ${ isRepo ? 'repositories' : 'developers' } for your choices.`} } /> ) : null} {backTop ? ( ) : null} ) } export default Index ================================================ FILE: src/pages/index/styles.scss ================================================ .back-top { display: flex; align-items: center; justify-content: center; height: 30px; width: 30px; border-radius: 100%; background-color: #0077fa; color: #fff; bottom: 20px; right: 10px; } .empty { height: 74vh; display: flex; flex-direction: column; align-items: center; justify-content: center; .empty-description { display: block; width: 80%; margin: 0 auto; } } ================================================ FILE: src/utils/common.js ================================================ export const truncate = (str, maxLength = 14) => { return str.length > maxLength ? `${str.substring(0, maxLength)}...` : str } export const convert = params => { if (!params) return params return Object.entries(params) .map(([key, value]) => { value = value === 'any' ? '' : value return [key, value] }) .reduce((final, item) => { const [key, value] = item final[key] = value return final }, {}) } export const numberWithCommas = number => { return number?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') } export const getTimeStamp = () => new Date().getTime() ================================================ FILE: src/utils/index.js ================================================ export {default as polyfill} from './polyfill' export * from './common' ================================================ FILE: src/utils/polyfill/electron/index.js ================================================ export * from './storage' export * from './utils' ================================================ FILE: src/utils/polyfill/electron/storage.js ================================================ /** * Saves storage with electron-store. * This storage communicates with electron's main.js * rather than browser's window.localStorage */ const {storage} = window.electron || {} export const setStorage = (key, value) => { try { storage?.set(key, value) } catch (err) { console.log(`An error occurred when setting storage ${key} with value: `, value, err) } } export const getStorage = key => { try { return storage?.get(key) } catch (err) { console.log(`An error occurred when getting storage ${key}.`, err) return null } } export const getContextFromStorage = () => { try { return storage?.store() || {} } catch (err) { console.log('An error occurred when getting context from storage.', err) return {} } } ================================================ FILE: src/utils/polyfill/electron/utils.js ================================================ const {open, receive, send} = window.electron || {} export {open, receive, send} ================================================ FILE: src/utils/polyfill/index.js ================================================ import * as web from './web' import * as electron from './electron' import {isElectron} from '@/config' export default isElectron ? electron : web ================================================ FILE: src/utils/polyfill/web/index.js ================================================ export * from './storage' export * from './utils' ================================================ FILE: src/utils/polyfill/web/storage.js ================================================ /** * Saves storage with localStorage. */ const storage = window.localStorage export const setStorage = (key, value) => { try { storage.setItem(key, JSON.stringify({value})) } catch (err) { console.log(`An error occurred when setting storage ${key} with value: `, value, err) } } export const getStorage = key => { try { return JSON.parse(storage.getItem(key)).value } catch (err) { console.log(`An error occurred when getting storage ${key}.`, err) return null } } export const getContextFromStorage = () => { try { const context = Object.keys(storage).reduce((final, key) => { final[key] = getStorage(key) return final }, {}) return context || {} } catch (err) { console.log('An error occurred when getting context from storage.', err) return {} } } ================================================ FILE: src/utils/polyfill/web/utils.js ================================================ export const open = window.open export const receive = () => () => {} export const send = () => {} ================================================ FILE: webpack/chrome/webpack.config.js ================================================ /** * The Webpack config which chrome extension project uses */ const path = require('path') const webpack = require('webpack') const {merge} = require('webpack-merge') const HtmlWebpackPlugin = require('html-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') const base = require('../webpack.base.config') const generateChromeManifest = require('../../chrome-manifest') generateChromeManifest() module.exports = (_, argv) => { console.log('webpack config argv =>', argv) const DEV = argv.mode === 'development' return merge(base(argv), { /** * The field `devtool` fixed unsafe-eval error in dev mode. * See https://stackoverflow.com/questions/48047150/chrome-extension-compiled-by-webpack-throws-unsafe-eval-error */ devtool: DEV ? 'cheap-module-source-map' : false, plugins: [ new webpack.DefinePlugin({ 'process.env': { WEBPACK_DEV: DEV, isChrome: true, }, }), new HtmlWebpackPlugin({ template: path.resolve('./src/index.ejs'), filename: 'index.html', chunks: ['main'], isChrome: true, analyticsDomain: DEV ? 'raise-dev.curve.to' : 'raise-chrome.curve.to', }), new CopyWebpackPlugin({ patterns: [ {from: './static/chrome', to: './static'}, {from: './src/chrome/manifest.json', to: './manifest.json'}, {from: './src/chrome', to: './chrome', globOptions: {ignore: ['**/*/manifest.json']}}, ], }), ], }) } ================================================ FILE: webpack/main/webpack.config.js ================================================ /** * The webpack config which Electron main uses */ const path = require('path') const CopyWebpackPlugin = require('copy-webpack-plugin') module.exports = { target: 'electron-main', entry: { main: path.resolve('./electron/main.js'), preload: path.resolve('./electron/preload.js'), }, output: { filename: '[name].js', path: path.resolve('./dist'), }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: 'babel-loader', }, { test: /.node$/, loader: 'node-loader', }, ], }, resolve: { symlinks: false, cacheWithContext: false, alias: { '@shared': path.resolve('./shared'), '@pkg': path.resolve('./package.json'), }, }, node: { __dirname: false, __filename: false, }, plugins: [ new CopyWebpackPlugin({ patterns: [{from: './static', to: './static'}], }), ], } ================================================ FILE: webpack/renderer/webpack.config.js ================================================ /** * The Webpack config which Electron's renderer uses */ const path = require('path') const webpack = require('webpack') const {merge} = require('webpack-merge') const HtmlWebpackPlugin = require('html-webpack-plugin') const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin') const base = require('../webpack.base.config') module.exports = (_, argv) => { console.log('webpack config argv =>', argv) const DEV = argv.mode === 'development' const PROD = !DEV return merge(base(argv), { plugins: [ new webpack.DefinePlugin({ 'process.env': { WEBPACK_DEV: DEV, }, }), new HtmlWebpackPlugin({ template: path.resolve('./src/index.ejs'), filename: 'index.html', chunks: ['main'], isElectron: true, analyticsDomain: DEV ? 'raise-dev.curve.to' : 'raise-desktop.curve.to', }), PROD && new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/^runtime.+\.js$/]), ].filter(Boolean), }) } ================================================ FILE: webpack/webpack.base.config.js ================================================ /** * The Webpack config which is shared by Electron's renderer and web */ const path = require('path') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin') const terserPluginConfig = { extractComments: false, terserOptions: { format: { comments: false, ascii_only: true, }, compress: { drop_console: true, }, }, } const splitChunksConfig = { chunks: 'all', cacheGroups: { default: { chunks: 'async', priority: 10, minChunks: 2, reuseExistingChunk: true, }, defaultVendors: false, commons: { chunks: 'all', test: /[\\/]node_modules[\\/]/, priority: 20, minChunks: 2, maxSize: 512 * 1024, // 512kb name: 'commons', filename: '[name].[chunkhash:8].js', reuseExistingChunk: true, }, core: { chunks: 'all', test: /node_modules[\\/](?:core-js|regenerator-runtime|@babel|(?:style|css)-loader)/, priority: 30, name: 'core', filename: '[name].[chunkhash:8].js', reuseExistingChunk: true, }, react: { chunks: 'all', test: /node_modules[\\/](?:react|react-dom|react-router-dom)/, priority: 100, name: 'react', filename: '[name].[chunkhash:8].js', reuseExistingChunk: true, }, }, } module.exports = argv => { const DEV = argv.mode === 'development' const PROD = !DEV return { devtool: DEV ? 'eval-cheap-module-source-map' : false, bail: PROD, cache: DEV ? {type: 'memory'} : { type: 'filesystem', buildDependencies: {config: [__filename]}, }, entry: ['./src/index.js'], output: { path: path.resolve('./dist'), filename: `[name]${PROD ? '.[contenthash:8]' : ''}.js`, chunkFilename: `[name]${PROD ? '.[contenthash:8]' : ''}.js`, publicPath: PROD ? './' : '', }, resolve: { symlinks: false, cacheWithContext: false, alias: { '@': path.resolve('./src'), '@static': path.resolve('./static'), '@shared': path.resolve('./shared'), '@pkg': path.resolve('./package.json'), }, }, devServer: { static: path.resolve('./dist'), port: 3000, }, optimization: PROD ? { runtimeChunk: 'single', chunkIds: 'deterministic', moduleIds: 'deterministic', minimizer: [ new TerserPlugin(terserPluginConfig), new CssMinimizerPlugin({test: /\.css$/}), ], splitChunks: splitChunksConfig, } : undefined, module: { rules: [ { test: /\.(js|jsx)$/, include: [path.resolve('./src')], use: [ { loader: 'babel-loader', options: { cacheDirectory: true, cacheCompression: false, }, }, ], }, { test: /\.css$/, exclude: /node_modules/, use: [ DEV ? 'style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { modules: { exportLocalsConvention: 'camelCase', localIdentName: '[name]__[local]___[hash:base64:5]', }, }, }, 'postcss-loader', ], }, { test: /\.css$/, include: /node_modules/, use: [DEV ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'], }, { test: /\.scss$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: { exportLocalsConvention: 'camelCase', localIdentName: '[name]__[local]___[hash:base64:5]', }, }, }, 'postcss-loader', 'sass-loader', ], }, { test: /\.(png|jpg|svg|gif)$/, type: 'asset', parser: { dataUrlCondition: { maxSize: 10 * 1024, // 10kb }, }, generator: { filename: 'assets/images/[name].[hash:8][ext][query]', }, }, ], }, plugins: [ PROD && new MiniCssExtractPlugin({ filename: 'assets/styles/[name].[contenthash:8].css', chunkFilename: 'assets/styles/[name].[contenthash:8].css', ignoreOrder: true, }), ].filter(Boolean), } }