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

94 | GitHub Trending 95 |

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