Repository: meetyan/raise
Branch: master
Commit: 7fd970899f2c
Files: 79
Total size: 98.7 KB
Directory structure:
gitextract_6pndr1_7/
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── babel.config.js
├── build/
│ ├── after-sign-hook.js
│ ├── icon.icns
│ └── mac/
│ └── entitlements.plist
├── chrome-manifest.js
├── electron/
│ ├── common.js
│ ├── config/
│ │ ├── constants.js
│ │ ├── index.js
│ │ └── store.js
│ ├── ipc/
│ │ └── index.js
│ ├── main.js
│ ├── preload.js
│ └── update-manager.js
├── jsconfig.json
├── package.json
├── shared/
│ ├── constants.js
│ └── index.js
├── src/
│ ├── app-context.js
│ ├── app.js
│ ├── app.scss
│ ├── assets/
│ │ └── styles/
│ │ ├── global.scss
│ │ └── reset.scss
│ ├── chrome/
│ │ ├── plausible.js
│ │ └── popup.js
│ ├── components/
│ │ ├── about-modal/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── developer-content/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── filter/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── index.js
│ │ ├── raise-header/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── repository-content/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── settings-modal/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── skeleton-placeholder/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── update-notification/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ └── upper-container/
│ │ └── index.js
│ ├── config/
│ │ ├── constants.js
│ │ ├── index.js
│ │ ├── languages.js
│ │ └── spoken-languages.js
│ ├── hooks/
│ │ ├── index.js
│ │ ├── use-context-props.js
│ │ ├── use-dock-icon.js
│ │ ├── use-mode.js
│ │ ├── use-outside-click.js
│ │ └── use-scroll-position.js
│ ├── index.ejs
│ ├── index.js
│ ├── io/
│ │ ├── index.js
│ │ ├── interceptor.js
│ │ └── trending.js
│ ├── lib/
│ │ ├── index.js
│ │ └── is-electron.js
│ ├── pages/
│ │ └── index/
│ │ ├── index.js
│ │ └── styles.scss
│ └── utils/
│ ├── common.js
│ ├── index.js
│ └── polyfill/
│ ├── electron/
│ │ ├── index.js
│ │ ├── storage.js
│ │ └── utils.js
│ ├── index.js
│ └── web/
│ ├── index.js
│ ├── storage.js
│ └── utils.js
└── webpack/
├── chrome/
│ └── webpack.config.js
├── main/
│ └── webpack.config.js
├── renderer/
│ └── webpack.config.js
└── webpack.base.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.js
================================================
module.exports = {
root: true,
env: {
browser: true,
node: true,
commonjs: true,
},
parser: '@babel/eslint-parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {jsx: true},
},
settings: {
'import/resolver': {
alias: {
map: [
['@', './src'],
['@static', './static'],
['@shared', './shared'],
['@pkg', './package.json'],
],
extensions: ['.js', '.jsx'],
},
},
react: {version: 'detect'},
},
extends: [
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:import/recommended',
'standard',
'plugin:prettier/recommended',
],
globals: {
chrome: true,
},
rules: {
'react/react-in-jsx-scope': 'off',
'prettier/prettier': [
'error',
{
arrowParens: 'avoid',
bracketSpacing: false,
printWidth: 100,
semi: false,
singleQuote: true,
endOfLine: 'auto',
},
],
'react/prop-types': 'off',
'react-hooks/exhaustive-deps': 'off',
'prefer-promise-reject-errors': [2, {allowEmptyReject: true}],
camelcase: ['error', {properties: 'never', ignoreDestructuring: true}],
},
}
================================================
FILE: .gitignore
================================================
node_modules
dist
out
src/chrome/manifest.json
.DS_Store
.eslintcache
.stylelintcache
.env
================================================
FILE: .prettierrc
================================================
{
"arrowParens": "avoid",
"bracketSpacing": false,
"printWidth": 100,
"semi": false,
"singleQuote": true
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 Jiajun Yan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Raise
A simple (and unofficial) GitHub Trending client that lives in your menubar.

## 📸 Screenshots

## 🖥 Installation
### New!! Raise is now available as a Chrome Extension
More in favor of a Chrome Extension variant? Check it out here:
<a href="https://chrome.google.com/webstore/detail/raise-github-trending/gehkhkcbbfeooflhkboaggpgldmhfmag?hl=en&authuser=0" target="_blank"><img src="./static/available-in-chrome-web-store.png" width="200" /></a>
Otherwise, download from [GitHub Releases](https://github.com/meetyan/raise/releases) and install.
Currently Raise can run on macOS and Windows machines.
### macOS
If you use an Intel machine, please download the `.zip` file with its filename containing no architecture. Otherwise use `arm64.zip` if your hardware is armed with Apple Silicon (M1/M2).
### Windows
For Windows users simply download the package with `.exe` extension.
If it's your first time to open Raise, you might see a screen saying `Windows protected your PC. Windows SmartScreen prevented an unrecognized app from start. Running this app might put your PC at risk.`. To bypass it, click `More Info` and then click `Run anyway`. This is simply because Raise on Windows is not yet [code signed](https://www.electronjs.org/docs/latest/tutorial/code-signing). Read [this](https://stackoverflow.com/questions/48946680/how-to-avoid-the-windows-defender-smartscreen-prevented-an-unrecognized-app-fro) for your information.
## 🙌🏻 Features
- 🌠 Showcasing GitHub's trending repos and developers
- 🗺 Simple and intuitive user interface
- 🌍 Language and date range filtering
- 🌗 Dark mode
- 💻 More under development
## 🛠 Tech Involved
- [Electron](https://electronjs.org/)
- [React](https://reactjs.org/)
- [Semi Design](https://semi.design/)
- [GitHub Trending API](https://github.com/huchenme/github-trending-api)
- [Plausible](https://plausible.io/)
- [PM2](https://pm2.keymetrics.io/)
- [Webpack](https://webpack.js.org/)
## 🧑🏻💻 How to Develop
Raise is developed on Node.js v16. Other Node.js versions have not been tested.
Run the following commands in `Terminal.app` on macOS or `PowerShell` on Windows:
```bash
yarn
yarn start
```
## 📢 Build and Deploy
To build and deploy, run the following:
```bash
yarn build
yarn release
```
================================================
FILE: babel.config.js
================================================
module.exports = {
presets: [['@babel/preset-env'], '@babel/preset-react'],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
[
'import',
{libraryName: 'licia', libraryDirectory: '', camel2DashComponentName: false},
'licia',
],
],
}
================================================
FILE: build/after-sign-hook.js
================================================
// See https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/
// See https://philo.dev/notarizing-your-electron-application/
require('dotenv').config()
const fs = require('fs')
const path = require('path')
const {notarize} = require('electron-notarize')
const pkg = require('../package.json')
module.exports = async function (params) {
if (process.platform !== 'darwin') {
return
}
console.log('afterSign hook triggered', params)
const appId = pkg.build.appId
const appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`)
if (!fs.existsSync(appPath)) {
console.log('skip')
return
}
console.log(`Notarizing ${appId} found at ${appPath}`)
try {
await notarize({
appBundleId: appId,
appPath: appPath,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
})
} catch (error) {
console.error(error)
}
console.log(`Done notarizing ${appId}`)
}
================================================
FILE: build/mac/entitlements.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
================================================
FILE: chrome-manifest.js
================================================
/**
* Dynamically generate manifest.json for Chrome Extension
*/
const fs = require('fs')
const path = require('path')
const pkg = require('./package.json')
module.exports = () => {
const manifest = {
name: pkg.chromeProductName,
description: pkg.chromeDescription,
version: pkg.version,
manifest_version: 3,
permissions: [],
action: {
default_popup: 'index.html',
default_icon: {
16: '/static/16.png',
32: '/static/32.png',
48: '/static/48.png',
128: '/static/128.png',
},
},
icons: {
16: '/static/16.png',
32: '/static/32.png',
48: '/static/48.png',
128: '/static/128.png',
},
}
fs.writeFileSync(path.resolve('./src/chrome/manifest.json'), JSON.stringify(manifest))
}
================================================
FILE: electron/common.js
================================================
import {app, BrowserWindow, Menu, shell, Tray} from 'electron'
import path from 'path'
import log from 'electron-log'
import {IPC_FUNCTION, STORAGE_KEY} from '@shared'
import pkg from '@pkg'
import {INDEX_URL, isMac, ICON, MENUBAR, store, isDev} from './config'
import {mb} from './main'
// See https://github.com/electron/electron/issues/19775.
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
export const browserWindowConfig = {
width: MENUBAR.WIDTH,
height: MENUBAR.HEIGHT,
webPreferences: {preload: path.join(__dirname, './preload.js')},
}
export const createWindow = () => {
const mainWindow = new BrowserWindow({
...browserWindowConfig,
icon: ICON.LOGO,
})
mainWindow.webContents.openDevTools()
mainWindow.loadURL(INDEX_URL.DEV)
return mainWindow
}
export const createMenu = () => {
const template = [
...(isMac
? [
{
label: app.name,
submenu: [
{
label: `About ${pkg.productName}`,
click: () => {
mb.showWindow()
mb.window.send(IPC_FUNCTION.SHOW_ABOUT_MODAL)
},
},
{type: 'separator'},
{
label: 'Preferences',
click: () => {
mb.showWindow()
mb.window.send(IPC_FUNCTION.SHOW_SETTINGS_MODAL)
},
},
{type: 'separator'},
{role: 'hide'},
{role: 'hideOthers'},
{role: 'unhide'},
{type: 'separator'},
{role: 'quit'},
],
},
]
: []),
{
label: 'View',
submenu: [
{role: 'reload'},
{role: 'forceReload'},
...(isDev ? [{role: 'toggleDevTools'}] : []),
],
},
{
role: 'help',
submenu: [
{
label: 'Website',
click: async () => {
await shell.openExternal(pkg.repository)
},
},
],
},
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
/**
* Creates a right-clickable tray
* See https://erikmartinjordan.com/menu-contextual-electron
*/
export const createTray = () => {
const tray = new Tray(ICON.MENU)
const contextMenu = Menu.buildFromTemplate([{role: 'quit'}])
tray.on('right-click', () => tray.popUpContextMenu(contextMenu))
return tray
}
export const showDockIconAtLogin = () => {
if (!isMac) return
const shouldShowDockIcon = store.get(STORAGE_KEY.SHOW_DOCK_ICON)
log.info('shouldShowDockIcon', shouldShowDockIcon)
// Dock icon persists in the dock except a user disables it
if (shouldShowDockIcon === false) {
app.dock.hide()
}
}
================================================
FILE: electron/config/constants.js
================================================
import path from 'path'
import is from 'electron-is'
export const BROWSER_WINDOW = {
WIDTH: 1440,
HEIGHT: 900,
}
export const MENUBAR = {
WIDTH: 400,
HEIGHT: 600,
}
export const INDEX_URL = {
DEV: 'http://localhost:3000',
PROD: `file://${path.join(__dirname, './index.html')}`,
}
export const isMac = is.macOS()
export const ICON = {
LOGO: path.join(__dirname, './static/logo.png'),
MENU: path.join(__dirname, `./static/${isMac ? 'menu-' : ''}logo.png`),
}
export const INTERVAL = {
UPDATE: 1000 * 60 * 60 * 24, // every 24 hours
}
export const isDev = is.dev()
================================================
FILE: electron/config/index.js
================================================
export * from './constants'
export * from './store'
================================================
FILE: electron/config/store.js
================================================
import Store from 'electron-store'
import {STORAGE_KEY} from '@shared'
export const store = new Store({
defaults: {
[STORAGE_KEY.ENABLE_AUTO_UPDATE]: true,
},
})
================================================
FILE: electron/ipc/index.js
================================================
import {app} from 'electron'
import {autoUpdater} from 'electron-updater'
import {isMac} from '../config'
export const handleShowDockIcon = (_, visible) => {
if (!isMac) return
if (visible) {
app.dock.show()
return
}
app.dock.hide()
}
export const handleQuitAndInstall = () => {
autoUpdater.quitAndInstall()
}
================================================
FILE: electron/main.js
================================================
import {app, ipcMain} from 'electron'
import {menubar} from 'menubar'
import {IPC_FUNCTION} from '@shared'
import pkg from '@pkg'
import {INDEX_URL, isMac, ICON, isDev} from './config'
import {handleQuitAndInstall, handleShowDockIcon} from './ipc'
import {
browserWindowConfig,
createMenu,
createTray,
// eslint-disable-next-line no-unused-vars
createWindow,
showDockIconAtLogin,
} from './common'
import updateManager from './update-manager'
app.setName(pkg.productName)
export let mb = null
let isFirstLoad = true
/**
* Shows app icon in dock on macOS
*/
if (isMac) {
app.dock.setIcon(ICON.LOGO)
app.dock.show()
}
app.whenReady().then(() => {
ipcMain.on(IPC_FUNCTION.SHOW_DOCK_ICON, handleShowDockIcon)
ipcMain.on(IPC_FUNCTION.QUIT_AND_INSTALL, handleQuitAndInstall)
mb = menubar({
icon: ICON.MENU,
index: isDev ? INDEX_URL.DEV : INDEX_URL.PROD,
browserWindow: {...browserWindowConfig, resizable: false},
preloadWindow: true,
tray: createTray(),
tooltip: pkg.productName,
})
createMenu(mb)
mb.on('ready', () => {
updateManager.init()
showDockIconAtLogin()
if (isDev) {
createWindow() // enable this if you need an extra window open
}
/**
* The setTimeout is used as a hack to show window on ready.
* Otherwise the window simply flashes and won't stay shown.
* See https://github.com/maxogden/menubar/issues/76.
*/
setTimeout(() => {
mb.showWindow()
}, 500)
})
mb.on('show', () => {
/**
* Reloads page after a long period of inactivity.
* Checks on every show() call (except when the app loads for the very first time).
*/
if (isFirstLoad) return (isFirstLoad = false)
mb.window.send(IPC_FUNCTION.RELOAD_AFTER_INACTIVITY)
})
})
================================================
FILE: electron/preload.js
================================================
import {contextBridge, ipcRenderer, shell} from 'electron'
import {IPC_FUNCTION} from '@shared'
import {store} from './config'
contextBridge.exposeInMainWorld('electron', {
storage: {
set: (key, val) => store.set(key, val),
get: key => store.get(key),
store: () => store.store,
},
open: url => shell.openExternal(url),
/**
* Wraps commonly used ipcRenderer methods with the followings.
* See: https://github.com/reZach/secure-electron-template/issues/43#issuecomment-772303787
*/
send: (channel, data) => {
if (Object.values(IPC_FUNCTION).includes(channel)) {
ipcRenderer.send(channel, data)
}
},
receive: (channel, func) => {
if (Object.values(IPC_FUNCTION).includes(channel)) {
const subscription = (_, ...args) => func(...args)
ipcRenderer.on(channel, subscription)
return () => {
ipcRenderer.removeListener(channel, subscription)
}
}
},
receiveOnce: (channel, func) => {
if (Object.values(IPC_FUNCTION).includes(channel)) {
ipcRenderer.once(channel, (event, ...args) => func(...args))
}
},
removeAllListeners: channel => {
if (Object.values(IPC_FUNCTION).includes(channel)) {
ipcRenderer.removeAllListeners(channel)
}
},
})
================================================
FILE: electron/update-manager.js
================================================
import {autoUpdater} from 'electron-updater'
import log from 'electron-log'
import {IPC_FUNCTION, STORAGE_KEY} from '@shared'
import {store, INTERVAL} from './config'
import {mb} from './main'
autoUpdater.logger = log
autoUpdater.logger.transports.file.level = 'info'
const onUpdateDownloaded = () => {
autoUpdater.on('update-downloaded', () => {
mb.window.send(IPC_FUNCTION.SHOW_UPDATE_NOTIFICATION)
})
}
const checkForUpdates = () => {
log.info('store info', store.store)
const shouldAutoUpdate = store.get(STORAGE_KEY.ENABLE_AUTO_UPDATE)
log.info('shouldAutoUpdate', shouldAutoUpdate)
if (!shouldAutoUpdate) {
log.info('AUTO_UPDATE is set to false. Abort auto update...')
return
}
autoUpdater.checkForUpdates()
}
const init = () => {
checkForUpdates()
/**
* Sets interval for periodical checks
*/
setInterval(checkForUpdates, INTERVAL.UPDATE)
/**
* Updater events
*/
onUpdateDownloaded()
}
export default {init, checkForUpdates}
================================================
FILE: jsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@static/*": ["./static/*"],
"@shared": ["./shared"],
"@pkg": ["./package.json"]
}
},
"typeAcquisition": {"include": ["chrome"]},
"exclude": ["node_modules", "dist"]
}
================================================
FILE: package.json
================================================
{
"name": "raise",
"productName": "Raise",
"chromeProductName": "Raise - GitHub Trending",
"version": "1.2.1",
"description": "A simple (and unofficial) GitHub Trending client that lives in your menubar",
"chromeDescription": "GitHub Trending at a glance",
"main": "dist/main.js",
"repository": "https://github.com/meetyan/raise.git",
"homepage": "./",
"author": "Jiajun Yan",
"license": "MIT",
"scripts": {
"start": "concurrently -k \"yarn start:main\" \"yarn start:renderer\"",
"start:main": "webpack --mode=development --config webpack/main/webpack.config.js && wait-on tcp:3000 && electron .",
"start:renderer": "webpack server --mode=development --config webpack/renderer/webpack.config.js --hot",
"start:chrome": "webpack --mode=development --config webpack/chrome/webpack.config.js --watch",
"build": "rimraf ./dist && yarn build:main && yarn build:renderer",
"build:main": "webpack --mode=production --config webpack/main/webpack.config.js --progress",
"build:renderer": "webpack --mode=production --config webpack/renderer/webpack.config.js --progress",
"build:chrome": "rimraf ./dist && webpack --mode=production --config webpack/chrome/webpack.config.js --progress",
"lint": "eslint 'src/**/*.{js,jsx}' 'electron/**/*.{js,jsx}' --cache --fix",
"package": "rimraf ./out && electron-builder build --mac --win --publish never",
"release": "rimraf ./out && electron-builder build --mac --win --publish always"
},
"dependencies": {
"@douyinfe/semi-icons": "^2.15.1",
"@douyinfe/semi-illustrations": "^2.16.0",
"@douyinfe/semi-ui": "^2.15.1",
"ahooks": "^3.7.0",
"axios": "^0.27.2",
"electron-is": "^3.0.0",
"electron-log": "^4.4.8",
"electron-store": "^8.1.0",
"electron-updater": "^5.2.1",
"lodash": "^4.17.21",
"menubar": "^9.2.1",
"nprogress": "^0.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/core": "^7.17.4",
"@babel/eslint-parser": "7.15.4",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"babel-loader": "^8.2.3",
"babel-plugin-import": "1.13.3",
"concurrently": "^7.3.0",
"copy-webpack-plugin": "10.2.0",
"css-loader": "^6.6.0",
"css-minimizer-webpack-plugin": "3.3.1",
"electron": "^19.0.9",
"electron-builder": "^23.3.3",
"electron-notarize": "^1.2.1",
"eslint": "7.32.0",
"eslint-config-prettier": "8.3.0",
"eslint-config-standard": "16.0.3",
"eslint-import-resolver-alias": "1.1.2",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "5.1.0",
"eslint-plugin-react": "^7.8.2",
"eslint-plugin-react-hooks": "^4.2.0",
"file-loader": "5.1.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "2.4.5",
"node-loader": "^2.0.0",
"node-sass": "^7.0.1",
"postcss": "8",
"postcss-loader": "^6.2.1",
"prettier": "^2.5.1",
"react-dev-utils": "12.0.0",
"rimraf": "^3.0.2",
"sass-loader": "^12.6.0",
"style-loader": "^3.3.1",
"wait-on": "^6.0.1",
"webpack": "^5.69.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4",
"webpack-merge": "^5.8.0"
},
"build": {
"productName": "Raise",
"appId": "to.curve.raise",
"afterSign": "./build/after-sign-hook.js",
"directories": {
"output": "out"
},
"mac": {
"mergeASARs": false,
"target": [
{
"target": "zip",
"arch": [
"x64",
"arm64"
]
}
],
"type": "distribution",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "build/mac/entitlements.plist",
"entitlementsInherit": "build/mac/entitlements.plist",
"publish": {
"provider": "github",
"owner": "meetyan",
"repo": "raise"
}
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64",
"ia32"
]
}
],
"publish": {
"provider": "github",
"owner": "meetyan",
"repo": "raise"
}
},
"files": [
"dist/**/*",
"node_modules/**/*"
],
"publish": {
"provider": "github",
"owner": "meetyan"
}
}
}
================================================
FILE: shared/constants.js
================================================
export const IPC_FUNCTION = {
SHOW_ABOUT_MODAL: 'show-about-modal',
SHOW_SETTINGS_MODAL: 'show-settings-modal',
SHOW_DOCK_ICON: 'show-dock-icon',
RELOAD_AFTER_INACTIVITY: 'reload-after-inactivity',
SHOW_UPDATE_NOTIFICATION: 'show-update-notification',
QUIT_AND_INSTALL: 'quit-and-install',
}
export const STORAGE_KEY = {
MODE: 'mode',
SHOW_BACK_TOP: 'show-back-top',
SHOW_DOCK_ICON: 'show-dock-icon',
TRENDING_TYPE: 'trending-type',
ENABLE_AUTO_UPDATE: 'enable-auto-update',
}
================================================
FILE: shared/index.js
================================================
export * from './constants'
================================================
FILE: src/app-context.js
================================================
import React, {useState, useContext, useEffect} from 'react'
import {polyfill} from './utils'
const {setStorage} = polyfill
export const AppContext = React.createContext({})
export const AppProvider = ({value, children}) => {
const [context, setContext] = useState(value)
useEffect(() => {
setContext(value)
}, [value])
return <AppContext.Provider value={[context, setContext]}>{children}</AppContext.Provider>
}
export const useAppContext = () => {
return useContext(AppContext)
}
export const useContextProp = propName => {
const [context, setContext] = useAppContext()
const [prop, _setProp] = useState(context[propName])
useEffect(() => {
_setProp(context[propName])
}, [context, propName])
const setProp = val => {
_setProp(val)
setStorage(propName, val)
setContext(preCtx => {
const data = {
...preCtx,
[propName]: val,
}
return data
})
}
return [prop, setProp]
}
================================================
FILE: src/app.js
================================================
import React, {useState} from 'react'
import {Divider, Layout, Toast, Typography} from '@douyinfe/semi-ui'
import {MODE, TRENDING_TYPE, Z_INDEX} from '@/config'
import {AppProvider} from '@/app-context'
import {UpdateNotification, UpperContainer} from '@/components'
import Index from '@/pages/index/index'
import {polyfill} from '@/utils'
import pkg from '@pkg'
import {STORAGE_KEY} from '@shared'
import 'nprogress/nprogress.css'
import '@/assets/styles/reset.scss'
import '@/assets/styles/global.scss'
import styles from '@/app.scss'
const {Text} = Typography
const {Footer} = Layout
Toast.config({zIndex: Z_INDEX.TOAST})
const {REPOSITORIES} = TRENDING_TYPE
const {getContextFromStorage} = polyfill
const App = () => {
const [context] = useState({
[STORAGE_KEY.MODE]: MODE.LIGHT, // system themes
[STORAGE_KEY.SHOW_BACK_TOP]: true,
[STORAGE_KEY.SHOW_DOCK_ICON]: true,
[STORAGE_KEY.ENABLE_AUTO_UPDATE]: true,
...getContextFromStorage(),
[STORAGE_KEY.TRENDING_TYPE]: REPOSITORIES,
})
return (
<AppProvider value={context}>
<Layout className={styles.layout}>
<UpperContainer>
<Index />
</UpperContainer>
<Footer id="footer">
<Divider />
<div className={styles.copyright}>
<Text strong>
© {new Date().getFullYear()} {pkg.productName}. All rights reserved.
</Text>
</div>
</Footer>
<UpdateNotification />
</Layout>
</AppProvider>
)
}
export default App
================================================
FILE: src/app.scss
================================================
::-webkit-scrollbar {
display: none;
}
.layout {
background-color: var(--semi-color-bg-0);
padding: 20px;
min-width: 400px;
}
.copyright {
padding-top: 10px;
display: flex;
flex-direction: column;
align-items: flex-start;
}
================================================
FILE: src/assets/styles/global.scss
================================================
body {
color: var(--semi-color-text-0);
background-color: var(--semi-color-bg-0);
}
:global(#nprogress .bar) {
z-index: 99999;
}
================================================
FILE: src/assets/styles/reset.scss
================================================
/* stylelint-disable */
/* http://meyerweb.com/eric/tools/css/reset/
v5.0.1 | 20191019
License: none (public domain)
*/
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
menu,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
main,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section {
display: block;
}
/* HTML5 hidden-attribute fix for newer browsers */
*[hidden] {
display: none;
}
body {
line-height: 1;
}
menu,
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote::before,
blockquote::after,
q::before,
q::after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
================================================
FILE: src/chrome/plausible.js
================================================
// eslint-disable-next-line
!function(){"use strict";var a=window.location,r=window.document,t=window.localStorage,o=r.currentScript,s=o.getAttribute("data-api")||new URL(o.src).origin+"/api/event",l=t&&t.plausible_ignore;function p(t){console.warn("Ignoring Event: "+t)}function e(t,e){if(/^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/.test(a.hostname)||"file:"===a.protocol)return p("localhost");if(!(window._phantom||window.__nightmare||window.navigator.webdriver||window.Cypress)){if("true"==l)return p("localStorage flag");var i={};i.n=t,i.u=a.href,i.d=o.getAttribute("data-domain"),i.r=r.referrer||null,i.w=window.innerWidth,e&&e.meta&&(i.m=JSON.stringify(e.meta)),e&&e.props&&(i.p=JSON.stringify(e.props));var n=new XMLHttpRequest;n.open("POST",s,!0),n.setRequestHeader("Content-Type","text/plain"),n.send(JSON.stringify(i)),n.onreadystatechange=function(){4==n.readyState&&e&&e.callback&&e.callback()}}}var i=window.plausible&&window.plausible.q||[];window.plausible=e;for(var n,w=0;w<i.length;w++)e.apply(this,i[w]);function d(){n!==a.pathname&&(n=a.pathname,e("pageview"))}var u,c=window.history;c.pushState&&(u=c.pushState,c.pushState=function(){u.apply(this,arguments),d()},window.addEventListener("popstate",d)),"prerender"===r.visibilityState?r.addEventListener("visibilitychange",function(){n||"visible"!==r.visibilityState||d()}):d()}();
================================================
FILE: src/chrome/popup.js
================================================
/**
* Set body's background color to either light or dark
* This prevents popup from flashing on app launch
*/
const lightColor = '#fff'
const darkColor = '#16161a'
const MODE = 'mode'
const getStorage = key => {
try {
return JSON.parse(window.localStorage.getItem(key)).value
} catch (err) {
console.log(`An error occurred when getting storage ${key}.`, err)
return null
}
}
const mode = getStorage(MODE)
document.body.style.backgroundColor = mode === 'dark' ? darkColor : lightColor
document.documentElement.style.width = '400px'
document.documentElement.style.minHeight = '599.9px' // not sure why setting height to 600px causes page not scrollable
================================================
FILE: src/components/about-modal/index.js
================================================
import React from 'react'
import {Divider, Modal, Typography} from '@douyinfe/semi-ui'
import {VERSION, Z_INDEX} from '@/config'
import pkg from '@pkg'
import Logo from '@static/logo-without-padding.png'
import {polyfill} from '@/utils'
import styles from './styles.scss'
const {Text, Title} = Typography
const {open} = polyfill
const AboutModal = ({visible, setVisible}) => {
return (
<Modal
title="About"
visible={visible}
onOk={() => setVisible(false)}
onCancel={() => setVisible(false)}
closeOnEsc={true}
width={350}
height="fit-content"
centered
footer={null}
zIndex={Z_INDEX.MODAL}
>
<Divider />
<div className={styles.aboutModal}>
<div className={styles.logo}>
<img src={Logo} alt="logo" />
<Title className={styles.title} heading={5}>
{pkg.productName}
</Title>
</div>
<Text className={`${styles.centerAligned} ${styles.version}`}>Version {VERSION}</Text>
<Text className={styles.centerAligned}>
A simple (and unofficial) GitHub Trending client that lives in your menubar.
</Text>
<div className={styles.copyright}>
<Text link className={styles.centerAligned} onClick={() => open(pkg.repository)}>
An open-source project by Jiajun Yan.
</Text>
<Text className={styles.centerAligned}>
Copyright © {new Date().getFullYear()} Raise. All rights reserved.
</Text>
</div>
</div>
</Modal>
)
}
export default AboutModal
================================================
FILE: src/components/about-modal/styles.scss
================================================
.about-modal {
display: flex;
flex-direction: column;
padding: 20px 0;
.logo {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
img {
width: 80px;
height: 80px;
}
}
.title {
margin-top: 10px;
}
.center-aligned {
text-align: center;
}
.version {
margin-bottom: 20px;
}
.copyright {
display: flex;
flex-direction: column;
margin: 20px 0 6px;
}
}
================================================
FILE: src/components/developer-content/index.js
================================================
import React from 'react'
import {Typography, Layout, Card, Space} from '@douyinfe/semi-ui'
import {IconBranch, IconCrown} from '@douyinfe/semi-icons'
import {SkeletonPlaceholder} from '@/components'
import {polyfill} from '@/utils'
import styles from './styles.scss'
const {Content} = Layout
const {Text, Title} = Typography
const {open} = polyfill
const AuthorHeader = ({item}) => (
<div className={styles.header}>
<img className={styles.avatar} src={item.avatar} onClick={() => open(item.url)} />
<Space vertical align="start" spacing={2}>
<Title link heading={6} onClick={() => open(item.url)}>
{item.name}
</Title>
<Text className={styles.cursor} onClick={() => open(item.url)}>
{item.username}
</Text>
</Space>
</div>
)
const DeveloperContent = ({list, loading}) => {
return (
<Content className={styles.content}>
{Array.from({length: 5}).map((_, index) => (
<SkeletonPlaceholder key={index} loading={loading} />
))}
{list.map(item => {
if (!item.repo) {
return (
<Card className={styles.developer} key={item.name}>
<div style={{display: 'flex', justifyContent: 'space-between'}}>
<AuthorHeader item={item} />
</div>
</Card>
)
}
return (
<Card key={item.name} title={<AuthorHeader item={item} />} className={styles.developer}>
<Space vertical align="start">
<Space>
<IconCrown /> <Text>Popular Repo</Text>
</Space>
<Space>
<IconBranch />{' '}
<Text link strong onClick={() => open(item.repo.url)}>
{item.repo.name}
</Text>
</Space>
{item.repo.description ? (
<Text className={styles.description}>{item.repo.description}</Text>
) : null}
</Space>
</Card>
)
})}
</Content>
)
}
export default DeveloperContent
================================================
FILE: src/components/developer-content/styles.scss
================================================
.content {
padding-top: 15px;
.developer {
width: 100%;
margin-bottom: 20px;
background-color: var(--semi-color-fill-0);
}
.header {
display: flex;
align-items: center;
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 10px;
cursor: pointer;
}
}
.description {
margin-top: 10px;
}
.cursor {
cursor: pointer;
}
}
================================================
FILE: src/components/filter/index.js
================================================
import React, {forwardRef, useImperativeHandle, useRef} from 'react'
import {Form} from '@douyinfe/semi-ui'
import {SINCE_ARRAY, SPOKEN_LANGUAGES, LANGUAGES, SINCE, TRENDING_TYPE, Z_INDEX} from '@/config'
import {truncate} from '@/utils'
import {useTrendingType} from '@/hooks'
import styles from './styles.scss'
const Filter = ({getList}, ref) => {
const api = useRef()
const [trendingType] = useTrendingType()
const isRepo = trendingType === TRENDING_TYPE.REPOSITORIES
useImperativeHandle(ref, () => ({
reset() {
api.current.reset()
},
}))
return (
<div className={styles.filter}>
<div className={styles.bottom}>
<Form
labelPosition="left"
labelAlign="left"
labelWidth={180}
onValueChange={getList}
getFormApi={formApi => (api.current = formApi)}
>
{isRepo ? (
<Form.Select
field="spoken_language_code"
initValue="any"
label="Spoken language"
className={styles.bottomSelect}
filter
zIndex={Z_INDEX.SELECT}
style={{width: '100%'}}
emptyContent="No matches"
>
{SPOKEN_LANGUAGES.map(item => {
return (
<Form.Select.Option key={item.name} value={item.urlParam}>
{truncate(item.name)}
</Form.Select.Option>
)
})}
</Form.Select>
) : null}
<Form.Select
field="language"
initValue="any"
label="Language"
className={styles.bottomSelect}
filter
zIndex={Z_INDEX.SELECT}
style={{width: '100%'}}
emptyContent="No matches"
>
{LANGUAGES.map(item => {
return (
<Form.Select.Option key={item.name} value={item.urlParam}>
{truncate(item.name)}
</Form.Select.Option>
)
})}
</Form.Select>
<Form.Select
field="since"
initValue={SINCE.DAILY}
label="Date range"
className={styles.bottomSelect}
filter
zIndex={Z_INDEX.SELECT}
style={{width: '100%'}}
emptyContent="No matches"
>
{SINCE_ARRAY.map(since => {
return (
<Form.Select.Option key={since.value} value={since.value}>
{since.name}
</Form.Select.Option>
)
})}
</Form.Select>
</Form>
</div>
</div>
)
}
export default forwardRef(Filter)
================================================
FILE: src/components/filter/styles.scss
================================================
.filter {
display: flex;
flex-direction: column;
.bottom {
display: flex;
flex-direction: column;
.bottom-select {
width: 100%;
}
}
.bottom-select-text {
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
================================================
FILE: src/components/index.js
================================================
export {default as RaiseHeader} from './raise-header'
export {default as DeveloperContent} from './developer-content'
export {default as RepositoryContent} from './repository-content'
export {default as SettingsModal} from './settings-modal'
export {default as AboutModal} from './about-modal'
export {default as SkeletonPlaceholder} from './skeleton-placeholder'
export {default as Filter} from './filter'
export {default as UpperContainer} from './upper-container'
export {default as UpdateNotification} from './update-notification'
================================================
FILE: src/components/raise-header/index.js
================================================
import React, {useEffect, useLayoutEffect, useRef, useState} from 'react'
import {Button, Collapsible, Divider, Layout, Typography} from '@douyinfe/semi-ui'
import {
IconFilter,
IconInfoCircle,
IconMoon,
IconRefresh,
IconSetting,
IconSun,
} from '@douyinfe/semi-icons'
import {Filter, SettingsModal, AboutModal} from '@/components'
import {MODE, TRENDING_TYPE, isMac} from '@/config'
import {useMode, useOutsideClick, useScrollPosition, useTrendingType} from '@/hooks'
import {IPC_FUNCTION} from '@shared'
import pkg from '@pkg'
import {polyfill} from '@/utils'
import Logo from '@static/logo-without-padding.png'
import styles from './styles.scss'
const {Header} = Layout
const {Text} = Typography
const {REPOSITORIES, DEVELOPERS} = TRENDING_TYPE
const {SHOW_ABOUT_MODAL, SHOW_SETTINGS_MODAL} = IPC_FUNCTION
const RaiseHeader = ({refresh, getList, resetList}) => {
const headerRef = useRef()
const filterRef = useRef()
const [mode, setMode] = useMode()
const [trendingType, setTrendingType] = useTrendingType()
const scrollPosition = useScrollPosition()
const [headerHeight, setHeaderHeight] = useState(0)
const [showFilter, setShowFilter] = useState(false)
const [settingsModalVisible, setSettingsModalVisible] = useState(false)
const [aboutModalVisible, setAboutModalVisible] = useState(false)
useOutsideClick(headerRef, () => setShowFilter(false))
const trendingTypeButtonConfig = buttonType => {
return trendingType === buttonType ? {type: 'primary', theme: 'solid'} : {}
}
const TrendingButton = ({type}) => {
return (
<Button
{...trendingTypeButtonConfig(type)}
className={styles.trendingTypeButton}
onClick={() => {
if (trendingType === type) return
resetList()
setTrendingType(type)
}}
>
{type}
</Button>
)
}
const toggleFilter = () => {
setShowFilter(!showFilter)
}
useLayoutEffect(() => {
const [headerComponent] = document.getElementsByClassName(styles.header)
setHeaderHeight(headerComponent.offsetHeight - 20)
}, [])
useEffect(() => {
setShowFilter(false)
filterRef.current.reset()
}, [trendingType])
useEffect(() => {
const {receive} = polyfill
receive(SHOW_ABOUT_MODAL, () => setAboutModalVisible(true))
receive(SHOW_SETTINGS_MODAL, () => setSettingsModalVisible(true))
}, [])
return (
<>
<div ref={headerRef}>
<Header
className={styles.header}
style={{
boxShadow: scrollPosition || showFilter ? '0 8px 24px -2px rgba(0, 0, 0, 0.2)' : 'none',
...(isMac ? {backgroundColor: 'initial'} : {}),
}}
>
<div className={styles.top}>
<h1 className={styles.heading}>
<Text strong>GitHub Trending</Text>
</h1>
<div className={styles.trendingType}>
<TrendingButton
type={REPOSITORIES}
setType={setTrendingType}
config={trendingTypeButtonConfig}
/>
<TrendingButton
type={DEVELOPERS}
setType={setTrendingType}
config={trendingTypeButtonConfig}
/>
</div>
</div>
<div className={styles.settings}>
<div className={styles.logo}>
<img src={Logo} alt="logo" />
<Text strong>{pkg.productName}</Text>
</div>
<div className={styles.top}>
<Button
theme="borderless"
icon={<IconRefresh />}
onClick={() => {
setShowFilter(false)
refresh()
}}
/>
<Button theme="borderless" icon={<IconFilter />} onClick={toggleFilter} />
<Button
theme="borderless"
icon={mode === MODE.LIGHT ? <IconMoon /> : <IconSun />}
onClick={() => setMode(mode === MODE.LIGHT ? MODE.DARK : MODE.LIGHT)}
/>
<Button
theme="borderless"
icon={<IconInfoCircle />}
onClick={() => setAboutModalVisible(true)}
/>
<Button
theme="borderless"
icon={<IconSetting />}
onClick={() => setSettingsModalVisible(true)}
/>
</div>
</div>
<Divider style={{opacity: !scrollPosition || showFilter ? 1 : 0}} />
<Collapsible isOpen={showFilter} keepDOM>
<Filter ref={filterRef} getList={getList} />
</Collapsible>
</Header>
</div>
<div style={{width: '100%', height: headerHeight || 0}}></div>
<SettingsModal visible={settingsModalVisible} setVisible={setSettingsModalVisible} />
<AboutModal visible={aboutModalVisible} setVisible={setAboutModalVisible} />
</>
)
}
export default RaiseHeader
================================================
FILE: src/components/raise-header/styles.scss
================================================
.header {
display: flex;
flex-direction: column;
width: 100%;
padding: 20px 20px 0;
position: fixed;
top: 0;
left: 0;
z-index: 9999;
background-color: var(--semi-color-bg-0);
backdrop-filter: saturate(180%) blur(30px);
transition: all 0.2s;
.top {
display: flex;
justify-content: space-between;
align-items: center;
}
.heading {
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
.heading-title {
margin-left: 5px;
}
}
.trending-type {
display: flex;
.trending-type-button:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.trending-type-button:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.settings {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 0 3px 0;
.logo {
display: flex;
align-items: center;
img {
width: 16px;
height: 16px;
margin-right: 4px;
}
}
.top {
display: flex;
}
}
}
================================================
FILE: src/components/repository-content/index.js
================================================
import React from 'react'
import {Typography, Layout, Card, Space, Tooltip} from '@douyinfe/semi-ui'
import {IconBranch, IconSourceControl, IconStar} from '@douyinfe/semi-icons'
import {URL} from '@/config'
import {numberWithCommas, polyfill} from '@/utils'
import {SkeletonPlaceholder} from '@/components'
import styles from './styles.scss'
const {Content} = Layout
const {Text} = Typography
const {open} = polyfill
const RepositoryContent = ({list, loading}) => {
return (
<Content className={styles.content}>
{Array.from({length: 5}).map((_, index) => (
<SkeletonPlaceholder key={index} loading={loading} />
))}
{list.map(item => {
return (
<Card
key={item.name + item.author}
title={
<div className={styles.repoHeader}>
<IconBranch />
<div className={styles.repoAuthor}>
<Text link onClick={() => open(`${URL.GITHUB}/${item.author}`)}>
{item.author}
</Text>
{' / '}
<Tooltip content={item.name} position="bottom">
<Text link strong onClick={() => open(item.url)}>
{item.name}
</Text>
</Tooltip>
</div>
</div>
}
className={styles.repo}
headerExtraContent={
<Space spacing={4}>
<span className={styles.languageColor} style={{background: item.languageColor}} />
<Text type="secondary">{item.language || 'Unknown'}</Text>
</Space>
}
>
{item.description ? (
<Text className={styles.description}>{item.description}</Text>
) : null}
<div className={styles.bottomArea}>
<div className={styles.top}>
<Space>
<Space className={styles.cursor} spacing={4}>
<IconStar />
<Text onClick={() => open(`${item.url}/stargazers`)}>
{numberWithCommas(item.stars)}
</Text>
</Space>
<Space />
<Space className={styles.cursor} spacing={4}>
<IconSourceControl />
<Text onClick={() => open(`${item.url}/network/members.${item.author}`)}>
{numberWithCommas(item.forks)}
</Text>
</Space>
</Space>
<Space spacing={4}>
<IconStar />
<Text>{numberWithCommas(item.currentPeriodStars)} stars today</Text>
</Space>
</div>
{item.builtBy?.length ? (
<div className={styles.bottom}>
<Space>
<Text>Built by</Text>
<div>
{item.builtBy?.map(builtByAuthor => {
return (
<img
className={styles.avatar}
src={builtByAuthor.avatar}
key={builtByAuthor.avatar}
onClick={() => open(builtByAuthor.href)}
/>
)
})}
</div>
</Space>
</div>
) : null}
</div>
</Card>
)
})}
</Content>
)
}
export default RepositoryContent
================================================
FILE: src/components/repository-content/styles.scss
================================================
.content {
padding-top: 15px;
.repo-header {
display: flex;
align-items: center;
}
.repo-author {
margin-left: 5px;
width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.repo {
width: 100%;
margin-bottom: 20px;
background-color: var(--semi-color-fill-0);
}
.language-color {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
}
.description {
display: block;
margin-bottom: 20px;
}
.bottom-area {
display: flex;
flex-direction: column;
.top {
display: flex;
justify-content: space-between;
align-items: center;
}
.avatar {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 4px;
cursor: pointer;
&:last-child {
margin-right: 0;
}
}
.bottom {
display: flex;
align-items: center;
margin-top: 10px;
}
}
.cursor {
cursor: pointer;
}
}
================================================
FILE: src/components/settings-modal/index.js
================================================
import React from 'react'
import {Button, Divider, Modal, Space, Switch, Typography} from '@douyinfe/semi-ui'
import {IconExternalOpen} from '@douyinfe/semi-icons'
import {useAutoUpdate, useBackTop, useDockIcon, useMode} from '@/hooks'
import {MODE, URL, VERSION, Z_INDEX, isMac, isElectron} from '@/config'
import {polyfill} from '@/utils'
import pkg from '@pkg'
import styles from './styles.scss'
const {Text} = Typography
const {open} = polyfill
const SettingsModal = ({visible, setVisible}) => {
const [mode, setMode] = useMode()
const [backTop, setBackTop] = useBackTop()
const [dockIcon, setDockIcon] = useDockIcon()
const [autoUpdate, setAutoUpdate] = useAutoUpdate()
return (
<Modal
title="Settings"
visible={visible}
onOk={() => setVisible(false)}
onCancel={() => setVisible(false)}
closeOnEsc={true}
width={350}
height="fit-content"
centered
footer={null}
zIndex={Z_INDEX.MODAL}
>
<Divider />
<div className={styles.settingsModal}>
<Space style={{width: '100%'}} vertical spacing="medium" align="start">
<div className={styles.settingsItem}>
<Text strong>Dark mode</Text>
<Switch
checked={mode === MODE.DARK}
onChange={e => {
setMode(e ? MODE.DARK : MODE.LIGHT)
}}
/>
</div>
<div className={styles.settingsItem}>
<Text strong>Show back to top button</Text>
<Switch checked={backTop} onChange={setBackTop} />
</div>
{isMac && isElectron ? (
<div className={styles.settingsItem}>
<Text strong>Show app icon in dock</Text>
<Switch checked={dockIcon} onChange={setDockIcon} />
</div>
) : null}
<Divider />
{isElectron ? (
<div className={styles.settingsItem}>
<Text strong>Automatic updates</Text>
<Switch checked={autoUpdate} onChange={setAutoUpdate} />
</div>
) : null}
<div className={styles.settingsItem}>
<Text strong>Changelog</Text>
<Button icon={<IconExternalOpen />} onClick={() => open(URL.CHANGELOG)}>
Open
</Button>
</div>
<div className={styles.settingsItem}>
<Text strong>Experiencing a bug?</Text>
<Button icon={<IconExternalOpen />} onClick={() => open(URL.ISSUE)}>
Submit an issue
</Button>
</div>
<Divider />
<Text type="tertiary" className={styles.version}>
{pkg.productName}, version {VERSION}
</Text>
</Space>
</div>
</Modal>
)
}
export default SettingsModal
================================================
FILE: src/components/settings-modal/styles.scss
================================================
.settings-modal {
padding: 10px 0 20px;
box-sizing: border-box;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.settings-item {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.version {
margin-bottom: 4px;
}
================================================
FILE: src/components/skeleton-placeholder/index.js
================================================
import React from 'react'
import {Skeleton} from '@douyinfe/semi-ui'
import styles from './styles.scss'
const SkeletonPlaceholder = ({loading}) => {
return (
<Skeleton
className={styles.skeleton}
placeholder={<Skeleton.Image />}
loading={loading}
active
/>
)
}
export default SkeletonPlaceholder
================================================
FILE: src/components/skeleton-placeholder/styles.scss
================================================
.skeleton {
width: 100%;
height: 200;
border-radius: 6;
margin-bottom: 20;
}
================================================
FILE: src/components/update-notification/index.js
================================================
import React, {useEffect, useState} from 'react'
import {Banner, Button} from '@douyinfe/semi-ui'
import {IPC_FUNCTION} from '@shared'
import {polyfill} from '@/utils'
import styles from './styles.scss'
const {send, receive} = polyfill
const {QUIT_AND_INSTALL, SHOW_UPDATE_NOTIFICATION} = IPC_FUNCTION
const UpdateNotification = () => {
const [visible, setVisible] = useState(false)
useEffect(() => {
receive(SHOW_UPDATE_NOTIFICATION, () => setVisible(true))
}, [])
if (!visible) return null
return (
<div className={styles.update}>
<Banner
type="info"
className={styles.updateBanner}
fullMode={false}
icon={null}
closeIcon={null}
title="Update Available"
description="An update has been downloaded. Would you like to restart to update now?"
>
<div className={styles.updateBtns}>
<Button theme="solid" type="tertiary" onClick={() => setVisible(false)}>
Later
</Button>
<Button
className={styles.btnConfirm}
theme="solid"
type="primary"
onClick={() => send(QUIT_AND_INSTALL)}
>
Restart Now
</Button>
</div>
</Banner>
</div>
)
}
export default UpdateNotification
================================================
FILE: src/components/update-notification/styles.scss
================================================
.update {
position: fixed;
bottom: 0;
left: 0;
z-index: 99999;
width: 100vw;
background-color: var(--semi-color-bg-0);
}
.update-banner {
padding: 20px;
}
.update-btns {
display: flex;
justify-content: flex-end;
}
.btn-confirm {
margin-left: 10px;
}
================================================
FILE: src/components/upper-container/index.js
================================================
import React, {useLayoutEffect, useState} from 'react'
const UpperContainer = ({children}) => {
const [footerHeight, setFooterHeight] = useState(0)
useLayoutEffect(() => {
const footerComponent = document.getElementById('footer')
setFooterHeight(footerComponent.offsetHeight)
}, [])
return <div style={{minHeight: `calc(100vh - ${footerHeight}px - 40px)`}}>{children}</div>
}
export default UpperContainer
================================================
FILE: src/config/constants.js
================================================
import pkg from '@pkg'
import {isElectron as checkIsElectron} from '@/lib'
export const VERSION = pkg.version
export const TRENDING_TYPE = {
REPOSITORIES: 'Repositories',
DEVELOPERS: 'Developers',
}
export const MODE = {
LIGHT: 'light',
DARK: 'dark',
}
export const SINCE = {
DAILY: 'daily',
WEEKLY: 'weekly',
MONTHLY: 'monthly',
}
export const SINCE_MAP = {
[SINCE.DAILY]: 'Today',
[SINCE.WEEKLY]: 'This week',
[SINCE.MONTHLY]: 'This month',
}
export const SINCE_ARRAY = [
{name: SINCE_MAP[SINCE.DAILY], value: SINCE.DAILY},
{name: SINCE_MAP[SINCE.WEEKLY], value: SINCE.WEEKLY},
{name: SINCE_MAP[SINCE.MONTHLY], value: SINCE.MONTHLY},
]
export const Z_INDEX = {
MODAL: 99999,
TOAST: 99999,
SELECT: 9999,
}
export const URL = {
GITHUB: 'https://github.com',
CHANGELOG: 'https://github.com/meetyan/raise/releases',
ISSUE: 'https://github.com/meetyan/raise/issues',
SERVER: 'https://trending.curve.to',
}
export const ALLOWED_TIME_OF_INACTIVITY = 1000 * 60 * 60 * 3 // 3 hours
export const isElectron = checkIsElectron()
export const isMac = window.navigator?.userAgentData?.platform.toUpperCase().includes('MAC')
export const isChrome = !!process.env.isChrome
export const isDev = !!process.env.WEBPACK_DEV
================================================
FILE: src/config/index.js
================================================
export {default as LANGUAGES} from './languages'
export {default as SPOKEN_LANGUAGES} from './spoken-languages'
export * from './constants'
================================================
FILE: src/config/languages.js
================================================
export default [
{urlParam: 'any', name: 'Any'},
{urlParam: '1c-enterprise', name: '1C Enterprise'},
{urlParam: 'abap', name: 'ABAP'},
{urlParam: 'abnf', name: 'ABNF'},
{urlParam: 'actionscript', name: 'ActionScript'},
{urlParam: 'ada', name: 'Ada'},
{urlParam: 'adobe-font-metrics', name: 'Adobe Font Metrics'},
{urlParam: 'agda', name: 'Agda'},
{urlParam: 'ags-script', name: 'AGS Script'},
{urlParam: 'alloy', name: 'Alloy'},
{urlParam: 'alpine-abuild', name: 'Alpine Abuild'},
{urlParam: 'ampl', name: 'AMPL'},
{urlParam: 'angelscript', name: 'AngelScript'},
{urlParam: 'ant-build-system', name: 'Ant Build System'},
{urlParam: 'antlr', name: 'ANTLR'},
{urlParam: 'apacheconf', name: 'ApacheConf'},
{urlParam: 'apex', name: 'Apex'},
{urlParam: 'api-blueprint', name: 'API Blueprint'},
{urlParam: 'apl', name: 'APL'},
{urlParam: 'apollo-guidance-computer', name: 'Apollo Guidance Computer'},
{urlParam: 'applescript', name: 'AppleScript'},
{urlParam: 'arc', name: 'Arc'},
{urlParam: 'asciidoc', name: 'AsciiDoc'},
{urlParam: 'asn.1', name: 'ASN.1'},
{urlParam: 'asp', name: 'ASP'},
{urlParam: 'aspectj', name: 'AspectJ'},
{urlParam: 'assembly', name: 'Assembly'},
{urlParam: 'ats', name: 'ATS'},
{urlParam: 'augeas', name: 'Augeas'},
{urlParam: 'autohotkey', name: 'AutoHotkey'},
{urlParam: 'autoit', name: 'AutoIt'},
{urlParam: 'awk', name: 'Awk'},
{urlParam: 'ballerina', name: 'Ballerina'},
{urlParam: 'batchfile', name: 'Batchfile'},
{urlParam: 'befunge', name: 'Befunge'},
{urlParam: 'bison', name: 'Bison'},
{urlParam: 'bitbake', name: 'BitBake'},
{urlParam: 'blade', name: 'Blade'},
{urlParam: 'blitzbasic', name: 'BlitzBasic'},
{urlParam: 'blitzmax', name: 'BlitzMax'},
{urlParam: 'bluespec', name: 'Bluespec'},
{urlParam: 'boo', name: 'Boo'},
{urlParam: 'brainfuck', name: 'Brainfuck'},
{urlParam: 'brightscript', name: 'Brightscript'},
{urlParam: 'bro', name: 'Bro'},
{urlParam: 'c', name: 'C'},
{urlParam: 'c%23', name: 'C#'},
{urlParam: 'c%2B%2B', name: 'C++'},
{urlParam: 'c-objdump', name: 'C-ObjDump'},
{urlParam: 'c2hs-haskell', name: 'C2hs Haskell'},
{urlParam: "cap'n-proto", name: "Cap'n Proto"},
{urlParam: 'cartocss', name: 'CartoCSS'},
{urlParam: 'ceylon', name: 'Ceylon'},
{urlParam: 'chapel', name: 'Chapel'},
{urlParam: 'charity', name: 'Charity'},
{urlParam: 'chuck', name: 'ChucK'},
{urlParam: 'cirru', name: 'Cirru'},
{urlParam: 'clarion', name: 'Clarion'},
{urlParam: 'clean', name: 'Clean'},
{urlParam: 'click', name: 'Click'},
{urlParam: 'clips', name: 'CLIPS'},
{urlParam: 'clojure', name: 'Clojure'},
{urlParam: 'closure-templates', name: 'Closure Templates'},
{urlParam: 'cmake', name: 'CMake'},
{urlParam: 'cobol', name: 'COBOL'},
{urlParam: 'coffeescript', name: 'CoffeeScript'},
{urlParam: 'coldfusion', name: 'ColdFusion'},
{urlParam: 'coldfusion-cfc', name: 'ColdFusion CFC'},
{urlParam: 'collada', name: 'COLLADA'},
{urlParam: 'common-lisp', name: 'Common Lisp'},
{urlParam: 'common-workflow-language', name: 'Common Workflow Language'},
{urlParam: 'component-pascal', name: 'Component Pascal'},
{urlParam: 'cool', name: 'Cool'},
{urlParam: 'coq', name: 'Coq'},
{urlParam: 'cpp-objdump', name: 'Cpp-ObjDump'},
{urlParam: 'creole', name: 'Creole'},
{urlParam: 'crystal', name: 'Crystal'},
{urlParam: 'cson', name: 'CSON'},
{urlParam: 'csound', name: 'Csound'},
{urlParam: 'csound-document', name: 'Csound Document'},
{urlParam: 'csound-score', name: 'Csound Score'},
{urlParam: 'css', name: 'CSS'},
{urlParam: 'csv', name: 'CSV'},
{urlParam: 'cuda', name: 'Cuda'},
{urlParam: 'cweb', name: 'CWeb'},
{urlParam: 'cycript', name: 'Cycript'},
{urlParam: 'cython', name: 'Cython'},
{urlParam: 'd', name: 'D'},
{urlParam: 'd-objdump', name: 'D-ObjDump'},
{urlParam: 'darcs-patch', name: 'Darcs Patch'},
{urlParam: 'dart', name: 'Dart'},
{urlParam: 'dataweave', name: 'DataWeave'},
{urlParam: 'desktop', name: 'desktop'},
{urlParam: 'diff', name: 'Diff'},
{urlParam: 'digital-command-language', name: 'DIGITAL Command Language'},
{urlParam: 'dm', name: 'DM'},
{urlParam: 'dns-zone', name: 'DNS Zone'},
{urlParam: 'dockerfile', name: 'Dockerfile'},
{urlParam: 'dogescript', name: 'Dogescript'},
{urlParam: 'dtrace', name: 'DTrace'},
{urlParam: 'dylan', name: 'Dylan'},
{urlParam: 'e', name: 'E'},
{urlParam: 'eagle', name: 'Eagle'},
{urlParam: 'easybuild', name: 'Easybuild'},
{urlParam: 'ebnf', name: 'EBNF'},
{urlParam: 'ec', name: 'eC'},
{urlParam: 'ecere-projects', name: 'Ecere Projects'},
{urlParam: 'ecl', name: 'ECL'},
{urlParam: 'eclipse', name: 'ECLiPSe'},
{urlParam: 'edje-data-collection', name: 'Edje Data Collection'},
{urlParam: 'edn', name: 'edn'},
{urlParam: 'eiffel', name: 'Eiffel'},
{urlParam: 'ejs', name: 'EJS'},
{urlParam: 'elixir', name: 'Elixir'},
{urlParam: 'elm', name: 'Elm'},
{urlParam: 'emacs-lisp', name: 'Emacs Lisp'},
{urlParam: 'emberscript', name: 'EmberScript'},
{urlParam: 'eq', name: 'EQ'},
{urlParam: 'erlang', name: 'Erlang'},
{urlParam: 'f%23', name: 'F#'},
{urlParam: 'factor', name: 'Factor'},
{urlParam: 'fancy', name: 'Fancy'},
{urlParam: 'fantom', name: 'Fantom'},
{urlParam: 'filebench-wml', name: 'Filebench WML'},
{urlParam: 'filterscript', name: 'Filterscript'},
{urlParam: 'fish', name: 'fish'},
{urlParam: 'flux', name: 'FLUX'},
{urlParam: 'formatted', name: 'Formatted'},
{urlParam: 'forth', name: 'Forth'},
{urlParam: 'fortran', name: 'Fortran'},
{urlParam: 'freemarker', name: 'FreeMarker'},
{urlParam: 'frege', name: 'Frege'},
{urlParam: 'g-code', name: 'G-code'},
{urlParam: 'game-maker-language', name: 'Game Maker Language'},
{urlParam: 'gams', name: 'GAMS'},
{urlParam: 'gap', name: 'GAP'},
{urlParam: 'gcc-machine-description', name: 'GCC Machine Description'},
{urlParam: 'gdb', name: 'GDB'},
{urlParam: 'gdscript', name: 'GDScript'},
{urlParam: 'genie', name: 'Genie'},
{urlParam: 'genshi', name: 'Genshi'},
{urlParam: 'gentoo-ebuild', name: 'Gentoo Ebuild'},
{urlParam: 'gentoo-eclass', name: 'Gentoo Eclass'},
{urlParam: 'gerber-image', name: 'Gerber Image'},
{urlParam: 'gettext-catalog', name: 'Gettext Catalog'},
{urlParam: 'gherkin', name: 'Gherkin'},
{urlParam: 'glsl', name: 'GLSL'},
{urlParam: 'glyph', name: 'Glyph'},
{urlParam: 'gn', name: 'GN'},
{urlParam: 'gnuplot', name: 'Gnuplot'},
{urlParam: 'go', name: 'Go'},
{urlParam: 'golo', name: 'Golo'},
{urlParam: 'gosu', name: 'Gosu'},
{urlParam: 'grace', name: 'Grace'},
{urlParam: 'gradle', name: 'Gradle'},
{urlParam: 'grammatical-framework', name: 'Grammatical Framework'},
{urlParam: 'graph-modeling-language', name: 'Graph Modeling Language'},
{urlParam: 'graphql', name: 'GraphQL'},
{urlParam: 'graphviz-(dot)', name: 'Graphviz (DOT)'},
{urlParam: 'groovy', name: 'Groovy'},
{urlParam: 'groovy-server-pages', name: 'Groovy Server Pages'},
{urlParam: 'hack', name: 'Hack'},
{urlParam: 'haml', name: 'Haml'},
{urlParam: 'handlebars', name: 'Handlebars'},
{urlParam: 'harbour', name: 'Harbour'},
{urlParam: 'haskell', name: 'Haskell'},
{urlParam: 'haxe', name: 'Haxe'},
{urlParam: 'hcl', name: 'HCL'},
{urlParam: 'hlsl', name: 'HLSL'},
{urlParam: 'html', name: 'HTML'},
{urlParam: 'html%2Bdjango', name: 'HTML+Django'},
{urlParam: 'html%2Becr', name: 'HTML+ECR'},
{urlParam: 'html%2Beex', name: 'HTML+EEX'},
{urlParam: 'html%2Berb', name: 'HTML+ERB'},
{urlParam: 'html%2Bphp', name: 'HTML+PHP'},
{urlParam: 'http', name: 'HTTP'},
{urlParam: 'hy', name: 'Hy'},
{urlParam: 'hyphy', name: 'HyPhy'},
{urlParam: 'idl', name: 'IDL'},
{urlParam: 'idris', name: 'Idris'},
{urlParam: 'igor-pro', name: 'IGOR Pro'},
{urlParam: 'inform-7', name: 'Inform 7'},
{urlParam: 'ini', name: 'INI'},
{urlParam: 'inno-setup', name: 'Inno Setup'},
{urlParam: 'io', name: 'Io'},
{urlParam: 'ioke', name: 'Ioke'},
{urlParam: 'irc-log', name: 'IRC log'},
{urlParam: 'isabelle', name: 'Isabelle'},
{urlParam: 'isabelle-root', name: 'Isabelle ROOT'},
{urlParam: 'j', name: 'J'},
{urlParam: 'jasmin', name: 'Jasmin'},
{urlParam: 'java', name: 'Java'},
{urlParam: 'java-server-pages', name: 'Java Server Pages'},
{urlParam: 'javascript', name: 'JavaScript'},
{urlParam: 'jflex', name: 'JFlex'},
{urlParam: 'jison', name: 'Jison'},
{urlParam: 'jison-lex', name: 'Jison Lex'},
{urlParam: 'jolie', name: 'Jolie'},
{urlParam: 'json', name: 'JSON'},
{urlParam: 'json5', name: 'JSON5'},
{urlParam: 'jsoniq', name: 'JSONiq'},
{urlParam: 'jsonld', name: 'JSONLD'},
{urlParam: 'jsx', name: 'JSX'},
{urlParam: 'julia', name: 'Julia'},
{urlParam: 'jupyter-notebook', name: 'Jupyter Notebook'},
{urlParam: 'kicad-layout', name: 'KiCad Layout'},
{urlParam: 'kicad-legacy-layout', name: 'KiCad Legacy Layout'},
{urlParam: 'kicad-schematic', name: 'KiCad Schematic'},
{urlParam: 'kit', name: 'Kit'},
{urlParam: 'kotlin', name: 'Kotlin'},
{urlParam: 'krl', name: 'KRL'},
{urlParam: 'labview', name: 'LabVIEW'},
{urlParam: 'lasso', name: 'Lasso'},
{urlParam: 'latte', name: 'Latte'},
{urlParam: 'lean', name: 'Lean'},
{urlParam: 'less', name: 'Less'},
{urlParam: 'lex', name: 'Lex'},
{urlParam: 'lfe', name: 'LFE'},
{urlParam: 'lilypond', name: 'LilyPond'},
{urlParam: 'limbo', name: 'Limbo'},
{urlParam: 'linker-script', name: 'Linker Script'},
{urlParam: 'linux-kernel-module', name: 'Linux Kernel Module'},
{urlParam: 'liquid', name: 'Liquid'},
{urlParam: 'literate-agda', name: 'Literate Agda'},
{urlParam: 'literate-coffeescript', name: 'Literate CoffeeScript'},
{urlParam: 'literate-haskell', name: 'Literate Haskell'},
{urlParam: 'livescript', name: 'LiveScript'},
{urlParam: 'llvm', name: 'LLVM'},
{urlParam: 'logos', name: 'Logos'},
{urlParam: 'logtalk', name: 'Logtalk'},
{urlParam: 'lolcode', name: 'LOLCODE'},
{urlParam: 'lookml', name: 'LookML'},
{urlParam: 'loomscript', name: 'LoomScript'},
{urlParam: 'lsl', name: 'LSL'},
{urlParam: 'lua', name: 'Lua'},
{urlParam: 'm', name: 'M'},
{urlParam: 'm4', name: 'M4'},
{urlParam: 'm4sugar', name: 'M4Sugar'},
{urlParam: 'makefile', name: 'Makefile'},
{urlParam: 'mako', name: 'Mako'},
{urlParam: 'markdown', name: 'Markdown'},
{urlParam: 'marko', name: 'Marko'},
{urlParam: 'mask', name: 'Mask'},
{urlParam: 'mathematica', name: 'Mathematica'},
{urlParam: 'matlab', name: 'Matlab'},
{urlParam: 'maven-pom', name: 'Maven POM'},
{urlParam: 'max', name: 'Max'},
{urlParam: 'maxscript', name: 'MAXScript'},
{urlParam: 'mediawiki', name: 'MediaWiki'},
{urlParam: 'mercury', name: 'Mercury'},
{urlParam: 'meson', name: 'Meson'},
{urlParam: 'metal', name: 'Metal'},
{urlParam: 'minid', name: 'MiniD'},
{urlParam: 'mirah', name: 'Mirah'},
{urlParam: 'modelica', name: 'Modelica'},
{urlParam: 'modula-2', name: 'Modula-2'},
{urlParam: 'module-management-system', name: 'Module Management System'},
{urlParam: 'monkey', name: 'Monkey'},
{urlParam: 'moocode', name: 'Moocode'},
{urlParam: 'moonscript', name: 'MoonScript'},
{urlParam: 'mql4', name: 'MQL4'},
{urlParam: 'mql5', name: 'MQL5'},
{urlParam: 'mtml', name: 'MTML'},
{urlParam: 'muf', name: 'MUF'},
{urlParam: 'mupad', name: 'mupad'},
{urlParam: 'myghty', name: 'Myghty'},
{urlParam: 'ncl', name: 'NCL'},
{urlParam: 'nearley', name: 'Nearley'},
{urlParam: 'nemerle', name: 'Nemerle'},
{urlParam: 'nesc', name: 'nesC'},
{urlParam: 'netlinx', name: 'NetLinx'},
{urlParam: 'netlinx%2Berb', name: 'NetLinx+ERB'},
{urlParam: 'netlogo', name: 'NetLogo'},
{urlParam: 'newlisp', name: 'NewLisp'},
{urlParam: 'nextflow', name: 'Nextflow'},
{urlParam: 'nginx', name: 'Nginx'},
{urlParam: 'nim', name: 'Nim'},
{urlParam: 'ninja', name: 'Ninja'},
{urlParam: 'nit', name: 'Nit'},
{urlParam: 'nix', name: 'Nix'},
{urlParam: 'nl', name: 'NL'},
{urlParam: 'nsis', name: 'NSIS'},
{urlParam: 'nu', name: 'Nu'},
{urlParam: 'numpy', name: 'NumPy'},
{urlParam: 'objdump', name: 'ObjDump'},
{urlParam: 'objective-c', name: 'Objective-C'},
{urlParam: 'objective-c%2B%2B', name: 'Objective-C++'},
{urlParam: 'objective-j', name: 'Objective-J'},
{urlParam: 'ocaml', name: 'OCaml'},
{urlParam: 'omgrofl', name: 'Omgrofl'},
{urlParam: 'ooc', name: 'ooc'},
{urlParam: 'opa', name: 'Opa'},
{urlParam: 'opal', name: 'Opal'},
{urlParam: 'opencl', name: 'OpenCL'},
{urlParam: 'openedge-abl', name: 'OpenEdge ABL'},
{urlParam: 'openrc-runscript', name: 'OpenRC runscript'},
{urlParam: 'openscad', name: 'OpenSCAD'},
{urlParam: 'opentype-feature-file', name: 'OpenType Feature File'},
{urlParam: 'org', name: 'Org'},
{urlParam: 'ox', name: 'Ox'},
{urlParam: 'oxygene', name: 'Oxygene'},
{urlParam: 'oz', name: 'Oz'},
{urlParam: 'p4', name: 'P4'},
{urlParam: 'pan', name: 'Pan'},
{urlParam: 'papyrus', name: 'Papyrus'},
{urlParam: 'parrot', name: 'Parrot'},
{urlParam: 'parrot-assembly', name: 'Parrot Assembly'},
{urlParam: 'parrot-internal-representation', name: 'Parrot Internal Representation'},
{urlParam: 'pascal', name: 'Pascal'},
{urlParam: 'pawn', name: 'PAWN'},
{urlParam: 'pep8', name: 'Pep8'},
{urlParam: 'perl', name: 'Perl'},
{urlParam: 'perl-6', name: 'Perl 6'},
{urlParam: 'php', name: 'PHP'},
{urlParam: 'pic', name: 'Pic'},
{urlParam: 'pickle', name: 'Pickle'},
{urlParam: 'picolisp', name: 'PicoLisp'},
{urlParam: 'piglatin', name: 'PigLatin'},
{urlParam: 'pike', name: 'Pike'},
{urlParam: 'plpgsql', name: 'PLpgSQL'},
{urlParam: 'plsql', name: 'PLSQL'},
{urlParam: 'pod', name: 'Pod'},
{urlParam: 'pogoscript', name: 'PogoScript'},
{urlParam: 'pony', name: 'Pony'},
{urlParam: 'postcss', name: 'PostCSS'},
{urlParam: 'postscript', name: 'PostScript'},
{urlParam: 'pov-ray-sdl', name: 'POV-Ray SDL'},
{urlParam: 'powerbuilder', name: 'PowerBuilder'},
{urlParam: 'powershell', name: 'PowerShell'},
{urlParam: 'processing', name: 'Processing'},
{urlParam: 'prolog', name: 'Prolog'},
{urlParam: 'propeller-spin', name: 'Propeller Spin'},
{urlParam: 'protocol-buffer', name: 'Protocol Buffer'},
{urlParam: 'public-key', name: 'Public Key'},
{urlParam: 'pug', name: 'Pug'},
{urlParam: 'puppet', name: 'Puppet'},
{urlParam: 'pure-data', name: 'Pure Data'},
{urlParam: 'purebasic', name: 'PureBasic'},
{urlParam: 'purescript', name: 'PureScript'},
{urlParam: 'python', name: 'Python'},
{urlParam: 'python-console', name: 'Python console'},
{urlParam: 'python-traceback', name: 'Python traceback'},
{urlParam: 'qmake', name: 'QMake'},
{urlParam: 'qml', name: 'QML'},
{urlParam: 'r', name: 'R'},
{urlParam: 'racket', name: 'Racket'},
{urlParam: 'ragel', name: 'Ragel'},
{urlParam: 'raml', name: 'RAML'},
{urlParam: 'rascal', name: 'Rascal'},
{urlParam: 'raw-token-data', name: 'Raw token data'},
{urlParam: 'rdoc', name: 'RDoc'},
{urlParam: 'realbasic', name: 'REALbasic'},
{urlParam: 'reason', name: 'Reason'},
{urlParam: 'rebol', name: 'Rebol'},
{urlParam: 'red', name: 'Red'},
{urlParam: 'redcode', name: 'Redcode'},
{urlParam: 'regular-expression', name: 'Regular Expression'},
{urlParam: "ren'py", name: "Ren'Py"},
{urlParam: 'renderscript', name: 'RenderScript'},
{urlParam: 'restructuredtext', name: 'reStructuredText'},
{urlParam: 'rexx', name: 'REXX'},
{urlParam: 'rhtml', name: 'RHTML'},
{urlParam: 'ring', name: 'Ring'},
{urlParam: 'rmarkdown', name: 'RMarkdown'},
{urlParam: 'robotframework', name: 'RobotFramework'},
{urlParam: 'roff', name: 'Roff'},
{urlParam: 'rouge', name: 'Rouge'},
{urlParam: 'rpc', name: 'RPC'},
{urlParam: 'rpm-spec', name: 'RPM Spec'},
{urlParam: 'ruby', name: 'Ruby'},
{urlParam: 'runoff', name: 'RUNOFF'},
{urlParam: 'rust', name: 'Rust'},
{urlParam: 'sage', name: 'Sage'},
{urlParam: 'saltstack', name: 'SaltStack'},
{urlParam: 'sas', name: 'SAS'},
{urlParam: 'sass', name: 'Sass'},
{urlParam: 'scala', name: 'Scala'},
{urlParam: 'scaml', name: 'Scaml'},
{urlParam: 'scheme', name: 'Scheme'},
{urlParam: 'scilab', name: 'Scilab'},
{urlParam: 'scss', name: 'SCSS'},
{urlParam: 'sed', name: 'sed'},
{urlParam: 'self', name: 'Self'},
{urlParam: 'shaderlab', name: 'ShaderLab'},
{urlParam: 'shell', name: 'Shell'},
{urlParam: 'shellsession', name: 'ShellSession'},
{urlParam: 'shen', name: 'Shen'},
{urlParam: 'slash', name: 'Slash'},
{urlParam: 'slim', name: 'Slim'},
{urlParam: 'smali', name: 'Smali'},
{urlParam: 'smalltalk', name: 'Smalltalk'},
{urlParam: 'smarty', name: 'Smarty'},
{urlParam: 'smt', name: 'SMT'},
{urlParam: 'solidity', name: 'Solidity'},
{urlParam: 'sourcepawn', name: 'SourcePawn'},
{urlParam: 'sparql', name: 'SPARQL'},
{urlParam: 'spline-font-database', name: 'Spline Font Database'},
{urlParam: 'sqf', name: 'SQF'},
{urlParam: 'sql', name: 'SQL'},
{urlParam: 'sqlpl', name: 'SQLPL'},
{urlParam: 'squirrel', name: 'Squirrel'},
{urlParam: 'srecode-template', name: 'SRecode Template'},
{urlParam: 'stan', name: 'Stan'},
{urlParam: 'standard-ml', name: 'Standard ML'},
{urlParam: 'stata', name: 'Stata'},
{urlParam: 'ston', name: 'STON'},
{urlParam: 'stylus', name: 'Stylus'},
{urlParam: 'sublime-text-config', name: 'Sublime Text Config'},
{urlParam: 'subrip-text', name: 'SubRip Text'},
{urlParam: 'sugarss', name: 'SugarSS'},
{urlParam: 'supercollider', name: 'SuperCollider'},
{urlParam: 'svg', name: 'SVG'},
{urlParam: 'swift', name: 'Swift'},
{urlParam: 'systemverilog', name: 'SystemVerilog'},
{urlParam: 'tcl', name: 'Tcl'},
{urlParam: 'tcsh', name: 'Tcsh'},
{urlParam: 'tea', name: 'Tea'},
{urlParam: 'terra', name: 'Terra'},
{urlParam: 'tex', name: 'TeX'},
{urlParam: 'text', name: 'Text'},
{urlParam: 'textile', name: 'Textile'},
{urlParam: 'thrift', name: 'Thrift'},
{urlParam: 'ti-program', name: 'TI Program'},
{urlParam: 'tla', name: 'TLA'},
{urlParam: 'toml', name: 'TOML'},
{urlParam: 'turing', name: 'Turing'},
{urlParam: 'turtle', name: 'Turtle'},
{urlParam: 'twig', name: 'Twig'},
{urlParam: 'txl', name: 'TXL'},
{urlParam: 'type-language', name: 'Type Language'},
{urlParam: 'typescript', name: 'TypeScript'},
{urlParam: 'unified-parallel-c', name: 'Unified Parallel C'},
{urlParam: 'unity3d-asset', name: 'Unity3D Asset'},
{urlParam: 'unix-assembly', name: 'Unix Assembly'},
{urlParam: 'uno', name: 'Uno'},
{urlParam: 'unrealscript', name: 'UnrealScript'},
{urlParam: 'urweb', name: 'UrWeb'},
{urlParam: 'vala', name: 'Vala'},
{urlParam: 'vcl', name: 'VCL'},
{urlParam: 'verilog', name: 'Verilog'},
{urlParam: 'vhdl', name: 'VHDL'},
{urlParam: 'vim-script', name: 'Vim script'},
{urlParam: 'visual-basic', name: 'Visual Basic'},
{urlParam: 'volt', name: 'Volt'},
{urlParam: 'vue', name: 'Vue'},
{urlParam: 'wavefront-material', name: 'Wavefront Material'},
{urlParam: 'wavefront-object', name: 'Wavefront Object'},
{urlParam: 'wdl', name: 'wdl'},
{urlParam: 'web-ontology-language', name: 'Web Ontology Language'},
{urlParam: 'webassembly', name: 'WebAssembly'},
{urlParam: 'webidl', name: 'WebIDL'},
{urlParam: 'wisp', name: 'wisp'},
{urlParam: 'world-of-warcraft-addon-data', name: 'World of Warcraft Addon Data'},
{urlParam: 'x10', name: 'X10'},
{urlParam: 'xbase', name: 'xBase'},
{urlParam: 'xc', name: 'XC'},
{urlParam: 'xcompose', name: 'XCompose'},
{urlParam: 'xml', name: 'XML'},
{urlParam: 'xojo', name: 'Xojo'},
{urlParam: 'xpages', name: 'XPages'},
{urlParam: 'xpm', name: 'XPM'},
{urlParam: 'xproc', name: 'XProc'},
{urlParam: 'xquery', name: 'XQuery'},
{urlParam: 'xs', name: 'XS'},
{urlParam: 'xslt', name: 'XSLT'},
{urlParam: 'xtend', name: 'Xtend'},
{urlParam: 'yacc', name: 'Yacc'},
{urlParam: 'yaml', name: 'YAML'},
{urlParam: 'yang', name: 'YANG'},
{urlParam: 'yara', name: 'YARA'},
{urlParam: 'zephir', name: 'Zephir'},
{urlParam: 'zimpl', name: 'Zimpl'},
]
================================================
FILE: src/config/spoken-languages.js
================================================
export default [
{urlParam: 'any', name: 'Any'},
{urlParam: 'ab', name: 'Abkhazian'},
{urlParam: 'aa', name: 'Afar'},
{urlParam: 'af', name: 'Afrikaans'},
{urlParam: 'ak', name: 'Akan'},
{urlParam: 'sq', name: 'Albanian'},
{urlParam: 'am', name: 'Amharic'},
{urlParam: 'ar', name: 'Arabic'},
{urlParam: 'an', name: 'Aragonese'},
{urlParam: 'hy', name: 'Armenian'},
{urlParam: 'as', name: 'Assamese'},
{urlParam: 'av', name: 'Avaric'},
{urlParam: 'ae', name: 'Avestan'},
{urlParam: 'ay', name: 'Aymara'},
{urlParam: 'az', name: 'Azerbaijani'},
{urlParam: 'bm', name: 'Bambara'},
{urlParam: 'ba', name: 'Bashkir'},
{urlParam: 'eu', name: 'Basque'},
{urlParam: 'be', name: 'Belarusian'},
{urlParam: 'bn', name: 'Bengali'},
{urlParam: 'bh', name: 'Bihari languages'},
{urlParam: 'bi', name: 'Bislama'},
{urlParam: 'bs', name: 'Bosnian'},
{urlParam: 'br', name: 'Breton'},
{urlParam: 'bg', name: 'Bulgarian'},
{urlParam: 'my', name: 'Burmese'},
{urlParam: 'ca', name: 'Catalan, Valencian'},
{urlParam: 'ch', name: 'Chamorro'},
{urlParam: 'ce', name: 'Chechen'},
{urlParam: 'ny', name: 'Chichewa, Chewa, Nyanja'},
{urlParam: 'zh', name: 'Chinese'},
{urlParam: 'cv', name: 'Chuvash'},
{urlParam: 'kw', name: 'Cornish'},
{urlParam: 'co', name: 'Corsican'},
{urlParam: 'cr', name: 'Cree'},
{urlParam: 'hr', name: 'Croatian'},
{urlParam: 'cs', name: 'Czech'},
{urlParam: 'da', name: 'Danish'},
{urlParam: 'dv', name: 'Divehi, Dhivehi, Maldivian'},
{urlParam: 'nl', name: 'Dutch, Flemish'},
{urlParam: 'dz', name: 'Dzongkha'},
{urlParam: 'en', name: 'English'},
{urlParam: 'eo', name: 'Esperanto'},
{urlParam: 'et', name: 'Estonian'},
{urlParam: 'ee', name: 'Ewe'},
{urlParam: 'fo', name: 'Faroese'},
{urlParam: 'fj', name: 'Fijian'},
{urlParam: 'fi', name: 'Finnish'},
{urlParam: 'fr', name: 'French'},
{urlParam: 'ff', name: 'Fulah'},
{urlParam: 'gl', name: 'Galician'},
{urlParam: 'ka', name: 'Georgian'},
{urlParam: 'de', name: 'German'},
{urlParam: 'el', name: 'Greek, Modern'},
{urlParam: 'gn', name: 'Guarani'},
{urlParam: 'gu', name: 'Gujarati'},
{urlParam: 'ht', name: 'Haitian, Haitian Creole'},
{urlParam: 'ha', name: 'Hausa'},
{urlParam: 'he', name: 'Hebrew'},
{urlParam: 'hz', name: 'Herero'},
{urlParam: 'hi', name: 'Hindi'},
{urlParam: 'ho', name: 'Hiri Motu'},
{urlParam: 'hu', name: 'Hungarian'},
{urlParam: 'ia', name: 'Interlingua (International Auxil...'},
{urlParam: 'id', name: 'Indonesian'},
{urlParam: 'ie', name: 'Interlingue, Occidental'},
{urlParam: 'ga', name: 'Irish'},
{urlParam: 'ig', name: 'Igbo'},
{urlParam: 'ik', name: 'Inupiaq'},
{urlParam: 'io', name: 'Ido'},
{urlParam: 'is', name: 'Icelandic'},
{urlParam: 'it', name: 'Italian'},
{urlParam: 'iu', name: 'Inuktitut'},
{urlParam: 'ja', name: 'Japanese'},
{urlParam: 'jv', name: 'Javanese'},
{urlParam: 'kl', name: 'Kalaallisut, Greenlandic'},
{urlParam: 'kn', name: 'Kannada'},
{urlParam: 'kr', name: 'Kanuri'},
{urlParam: 'ks', name: 'Kashmiri'},
{urlParam: 'kk', name: 'Kazakh'},
{urlParam: 'km', name: 'Central Khmer'},
{urlParam: 'ki', name: 'Kikuyu, Gikuyu'},
{urlParam: 'rw', name: 'Kinyarwanda'},
{urlParam: 'ky', name: 'Kirghiz, Kyrgyz'},
{urlParam: 'kv', name: 'Komi'},
{urlParam: 'kg', name: 'Kongo'},
{urlParam: 'ko', name: 'Korean'},
{urlParam: 'ku', name: 'Kurdish'},
{urlParam: 'kj', name: 'Kuanyama, Kwanyama'},
{urlParam: 'la', name: 'Latin'},
{urlParam: 'lb', name: 'Luxembourgish, Letzeburgesch'},
{urlParam: 'lg', name: 'Ganda'},
{urlParam: 'li', name: 'Limburgan, Limburger, Limburgish'},
{urlParam: 'ln', name: 'Lingala'},
{urlParam: 'lo', name: 'Lao'},
{urlParam: 'lt', name: 'Lithuanian'},
{urlParam: 'lu', name: 'Luba-Katanga'},
{urlParam: 'lv', name: 'Latvian'},
{urlParam: 'gv', name: 'Manx'},
{urlParam: 'mk', name: 'Macedonian'},
{urlParam: 'mg', name: 'Malagasy'},
{urlParam: 'ms', name: 'Malay'},
{urlParam: 'ml', name: 'Malayalam'},
{urlParam: 'mt', name: 'Maltese'},
{urlParam: 'mi', name: 'Maori'},
{urlParam: 'mr', name: 'Marathi'},
{urlParam: 'mh', name: 'Marshallese'},
{urlParam: 'mn', name: 'Mongolian'},
{urlParam: 'na', name: 'Nauru'},
{urlParam: 'nv', name: 'Navajo, Navaho'},
{urlParam: 'nd', name: 'North Ndebele'},
{urlParam: 'ne', name: 'Nepali'},
{urlParam: 'ng', name: 'Ndonga'},
{urlParam: 'nb', name: 'Norwegian Bokmål'},
{urlParam: 'nn', name: 'Norwegian Nynorsk'},
{urlParam: 'no', name: 'Norwegian'},
{urlParam: 'ii', name: 'Sichuan Yi, Nuosu'},
{urlParam: 'nr', name: 'South Ndebele'},
{urlParam: 'oc', name: 'Occitan'},
{urlParam: 'oj', name: 'Ojibwa'},
{urlParam: 'cu', name: 'Church Slavic, Old Slavonic, Chu...'},
{urlParam: 'om', name: 'Oromo'},
{urlParam: 'or', name: 'Oriya'},
{urlParam: 'os', name: 'Ossetian, Ossetic'},
{urlParam: 'pa', name: 'Punjabi, Panjabi'},
{urlParam: 'pi', name: 'Pali'},
{urlParam: 'fa', name: 'Persian'},
{urlParam: 'pl', name: 'Polish'},
{urlParam: 'ps', name: 'Pashto, Pushto'},
{urlParam: 'pt', name: 'Portuguese'},
{urlParam: 'qu', name: 'Quechua'},
{urlParam: 'rm', name: 'Romansh'},
{urlParam: 'rn', name: 'Rundi'},
{urlParam: 'ro', name: 'Romanian, Moldavian, Moldovan'},
{urlParam: 'ru', name: 'Russian'},
{urlParam: 'sa', name: 'Sanskrit'},
{urlParam: 'sc', name: 'Sardinian'},
{urlParam: 'sd', name: 'Sindhi'},
{urlParam: 'se', name: 'Northern Sami'},
{urlParam: 'sm', name: 'Samoan'},
{urlParam: 'sg', name: 'Sango'},
{urlParam: 'sr', name: 'Serbian'},
{urlParam: 'gd', name: 'Gaelic, Scottish Gaelic'},
{urlParam: 'sn', name: 'Shona'},
{urlParam: 'si', name: 'Sinhala, Sinhalese'},
{urlParam: 'sk', name: 'Slovak'},
{urlParam: 'sl', name: 'Slovenian'},
{urlParam: 'so', name: 'Somali'},
{urlParam: 'st', name: 'Southern Sotho'},
{urlParam: 'es', name: 'Spanish, Castilian'},
{urlParam: 'su', name: 'Sundanese'},
{urlParam: 'sw', name: 'Swahili'},
{urlParam: 'ss', name: 'Swati'},
{urlParam: 'sv', name: 'Swedish'},
{urlParam: 'ta', name: 'Tamil'},
{urlParam: 'te', name: 'Telugu'},
{urlParam: 'tg', name: 'Tajik'},
{urlParam: 'th', name: 'Thai'},
{urlParam: 'ti', name: 'Tigrinya'},
{urlParam: 'bo', name: 'Tibetan'},
{urlParam: 'tk', name: 'Turkmen'},
{urlParam: 'tl', name: 'Tagalog'},
{urlParam: 'tn', name: 'Tswana'},
{urlParam: 'to', name: 'Tonga (Tonga Islands)'},
{urlParam: 'tr', name: 'Turkish'},
{urlParam: 'ts', name: 'Tsonga'},
{urlParam: 'tt', name: 'Tatar'},
{urlParam: 'tw', name: 'Twi'},
{urlParam: 'ty', name: 'Tahitian'},
{urlParam: 'ug', name: 'Uighur, Uyghur'},
{urlParam: 'uk', name: 'Ukrainian'},
{urlParam: 'ur', name: 'Urdu'},
{urlParam: 'uz', name: 'Uzbek'},
{urlParam: 've', name: 'Venda'},
{urlParam: 'vi', name: 'Vietnamese'},
{urlParam: 'vo', name: 'Volapük'},
{urlParam: 'wa', name: 'Walloon'},
{urlParam: 'cy', name: 'Welsh'},
{urlParam: 'wo', name: 'Wolof'},
{urlParam: 'fy', name: 'Western Frisian'},
{urlParam: 'xh', name: 'Xhosa'},
{urlParam: 'yi', name: 'Yiddish'},
{urlParam: 'yo', name: 'Yoruba'},
{urlParam: 'za', name: 'Zhuang, Chuang'},
{urlParam: 'zu', name: 'Zulu'},
]
================================================
FILE: src/hooks/index.js
================================================
export {default as useMode} from './use-mode'
export {default as useDockIcon} from './use-dock-icon'
export {default as useOutsideClick} from './use-outside-click'
export {default as useScrollPosition} from './use-scroll-position'
export * from './use-context-props'
================================================
FILE: src/hooks/use-context-props.js
================================================
import {useContextProp} from '@/app-context'
import {STORAGE_KEY} from '@shared'
export const useTrendingType = () => {
return useContextProp(STORAGE_KEY.TRENDING_TYPE)
}
export const useBackTop = () => {
return useContextProp(STORAGE_KEY.SHOW_BACK_TOP)
}
export const useAutoUpdate = () => {
return useContextProp(STORAGE_KEY.ENABLE_AUTO_UPDATE)
}
================================================
FILE: src/hooks/use-dock-icon.js
================================================
import {useContextProp} from '@/app-context'
import {polyfill} from '@/utils'
import {IPC_FUNCTION, STORAGE_KEY} from '@shared'
const useDockIcon = () => {
const [dockIcon, setDockIcon] = useContextProp(STORAGE_KEY.SHOW_DOCK_ICON)
const _setDockIcon = visible => {
polyfill.send(IPC_FUNCTION.SHOW_DOCK_ICON, visible)
setDockIcon(visible)
}
return [dockIcon, _setDockIcon]
}
export default useDockIcon
================================================
FILE: src/hooks/use-mode.js
================================================
import {useContextProp} from '@/app-context'
import {MODE} from '@/config'
import {STORAGE_KEY} from '@shared'
const useMode = () => {
const [mode, setMode] = useContextProp(STORAGE_KEY.MODE)
const _setMode = target => {
const body = document.body
if (target === MODE.LIGHT) {
body.removeAttribute('theme-mode')
setMode(MODE.LIGHT)
} else {
body.setAttribute('theme-mode', 'dark')
setMode(MODE.DARK)
}
}
return [mode, _setMode]
}
export default useMode
================================================
FILE: src/hooks/use-outside-click.js
================================================
import {useEffect} from 'react'
/**
* Executes a handler function on click outside of a specific div
* See: https://stackoverflow.com/questions/32553158/detect-click-outside-react-component
* @param {*} ref
* @param {*} handler
*/
const useOutsideClick = (ref, handler) => {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
handler()
}
}
// Bind the event listener
document.addEventListener('mousedown', handleClickOutside)
return () => {
// Unbind the event listener on clean up
document.removeEventListener('mousedown', handleClickOutside)
}
}, [ref])
}
export default useOutsideClick
================================================
FILE: src/hooks/use-scroll-position.js
================================================
import {useScroll} from 'ahooks'
const useScrollPosition = () => {
const scrollRef = useScroll()
const scrollPosition = scrollRef?.top
return scrollPosition
}
export default useScrollPosition
================================================
FILE: src/index.ejs
================================================
<html lang="en">
<head>
<meta charset="UTF-8">
<meta
http-equiv="X-UA-Compatible"
content="IE=edge"
>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>Raise - GitHub Trending</title>
</head>
<body>
<div id="root"></div>
<% if (htmlWebpackPlugin.options.isChrome) { %>
<script src="./chrome/popup.js"></script>
<script
defer
data-domain="<%= htmlWebpackPlugin.options.analyticsDomain %>"
data-api="http://analytics.curve.to/api/event"
src="./chrome/plausible.js">
</script>
<% } %>
<% if (htmlWebpackPlugin.options.isElectron) { %>
<script
defer
data-domain="<%= htmlWebpackPlugin.options.analyticsDomain %>"
data-api="http://analytics.curve.to/api/event"
src="https://analytics.curve.to/js/script.local.js">
</script>
<% } %>
</body>
</html>
================================================
FILE: src/index.js
================================================
import React from 'react'
import {createRoot} from 'react-dom/client'
import App from './app'
const container = document.querySelector('#root')
const root = createRoot(container)
root.render(<App />)
================================================
FILE: src/io/index.js
================================================
export * from './trending'
================================================
FILE: src/io/interceptor.js
================================================
import axios from 'axios'
import NProgress from 'nprogress'
NProgress.configure({showSpinner: false})
axios.interceptors.request.use(
config => {
NProgress.start()
return config
},
error => {
NProgress.start()
return Promise.reject(error)
}
)
axios.interceptors.response.use(
response => {
NProgress.done()
return response
},
error => {
NProgress.done()
return Promise.reject(error)
}
)
export default axios
================================================
FILE: src/io/trending.js
================================================
import {snakeCase} from 'lodash'
import axios from './interceptor'
import {TRENDING_TYPE, URL} from '@/config'
import {getTimeStamp} from '@/utils'
let controller
export let lastTimestamp = 0
const buildUrl = (baseUrl, params = {}) => {
const queryString = Object.keys(params)
.filter(key => params[key])
.map(key => `${snakeCase(key)}=${params[key]}`)
.join('&')
return queryString === '' ? baseUrl : `${baseUrl}?${queryString}`
}
const checkResponse = res => {
if (res.status !== 200) {
throw new Error('Something went wrong')
}
}
const fetch = async ({params = {}, type, serverUrl = URL.SERVER} = {}) => {
if (controller) {
controller.abort() // Makes sure that users always get the latest result
}
controller = new AbortController()
/**
* Used to compare between now and inactivity.
* Reloads if time of inactivity is too long
*/
lastTimestamp = getTimeStamp()
const res = await axios({
method: 'get',
url: buildUrl(`${serverUrl}/${type}`, params),
signal: controller.signal,
})
checkResponse(res)
return res.data
}
export const fetchRepositories = (params, serverUrl = URL.SERVER) => {
return fetch({params, serverUrl, type: TRENDING_TYPE.REPOSITORIES.toLocaleLowerCase()})
}
export const fetchDevelopers = async (params, serverUrl = URL.SERVER) => {
return fetch({params, serverUrl, type: TRENDING_TYPE.DEVELOPERS.toLocaleLowerCase()})
}
================================================
FILE: src/lib/index.js
================================================
export {default as isElectron} from './is-electron'
================================================
FILE: src/lib/is-electron.js
================================================
const isElectron = () => {
// Renderer process
if (
typeof window !== 'undefined' &&
typeof window.process === 'object' &&
window.process.type === 'renderer'
) {
return true
}
// Main process
if (
typeof process !== 'undefined' &&
typeof process.versions === 'object' &&
!!process.versions.electron
) {
return true
}
// Detect the user agent when the `nodeIntegration` option is set to false
if (
typeof navigator === 'object' &&
typeof navigator.userAgent === 'string' &&
navigator.userAgent.indexOf('Electron') >= 0
) {
return true
}
return false
}
export default isElectron
================================================
FILE: src/pages/index/index.js
================================================
import React, {useEffect, useState} from 'react'
import {Typography, BackTop, Toast, Empty} from '@douyinfe/semi-ui'
import {IconArrowUp} from '@douyinfe/semi-icons'
import {IllustrationNoResult, IllustrationNoResultDark} from '@douyinfe/semi-illustrations'
import axios from 'axios'
import {RaiseHeader, RepositoryContent, DeveloperContent} from '@/components'
import {fetchRepositories, fetchDevelopers, lastTimestamp} from '@/io'
import {convert, polyfill} from '@/utils'
import {ALLOWED_TIME_OF_INACTIVITY, TRENDING_TYPE} from '@/config'
import {useBackTop, useMode, useTrendingType} from '@/hooks'
import {IPC_FUNCTION} from '@shared'
import styles from './styles.scss'
const {Text} = Typography
const {REPOSITORIES} = TRENDING_TYPE
const {RELOAD_AFTER_INACTIVITY} = IPC_FUNCTION
const Index = () => {
const [trendingType] = useTrendingType()
const [backTop] = useBackTop()
const [mode, setMode] = useMode()
const [list, setList] = useState([])
const [getListParams, setGetListParams] = useState({})
const [loading, setLoading] = useState(false)
const [empty, setEmpty] = useState(false)
const isRepo = trendingType === REPOSITORIES
const Content = isRepo ? RepositoryContent : DeveloperContent
const resetList = () => setList([])
const getList = async params => {
window.scrollTo({top: 0})
setLoading(true)
resetList()
setGetListParams(params)
setEmpty(false)
let isCancel = false
try {
const fetch = isRepo ? fetchRepositories : fetchDevelopers
const res = await fetch(convert(params))
setList(res)
setEmpty(!res.length)
} catch (error) {
// Makes sure when a request is canceled, loading is still true for the next getList call
if (axios.isCancel(error)) return (isCancel = true)
console.log('An error occurred when calling getList. Params: ', params, error)
Toast.error(
'Oops. It looks like an error occurs. The server might be down. Please try again.'
)
} finally {
setLoading(isCancel)
}
}
const refresh = () => {
getList(getListParams)
}
useEffect(() => {
getList()
}, [trendingType])
/**
* Recover settings to last state according to context storage
* e.g., when a user toggles settings in the settings modal,
* a few changes have been made.
* After he closes the app and reopens it,
* all settings/context will have to be recovered.
*/
useEffect(() => {
setMode(mode)
}, [])
useEffect(() => {
const {receive} = polyfill
const removeReloadListener = receive(RELOAD_AFTER_INACTIVITY, () => {
const now = new Date().getTime()
if (lastTimestamp && now - lastTimestamp > ALLOWED_TIME_OF_INACTIVITY) {
getList(getListParams)
}
})
return () => {
removeReloadListener()
}
}, [getListParams])
return (
<>
<RaiseHeader refresh={refresh} getList={getList} resetList={resetList} />
<Content list={list} getList={getList} loading={loading} />
{empty ? (
<Empty
className={styles.empty}
image={<IllustrationNoResult style={{width: 150, height: 150}} />}
darkModeImage={<IllustrationNoResultDark style={{width: 150, height: 150}} />}
description={
<Text className={styles.emptyDescription}>
{`It looks like we don’t have any trending ${
isRepo ? 'repositories' : 'developers'
} for your choices.`}
</Text>
}
/>
) : null}
{backTop ? (
<BackTop className={styles.backTop}>
<IconArrowUp />
</BackTop>
) : null}
</>
)
}
export default Index
================================================
FILE: src/pages/index/styles.scss
================================================
.back-top {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
width: 30px;
border-radius: 100%;
background-color: #0077fa;
color: #fff;
bottom: 20px;
right: 10px;
}
.empty {
height: 74vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.empty-description {
display: block;
width: 80%;
margin: 0 auto;
}
}
================================================
FILE: src/utils/common.js
================================================
export const truncate = (str, maxLength = 14) => {
return str.length > maxLength ? `${str.substring(0, maxLength)}...` : str
}
export const convert = params => {
if (!params) return params
return Object.entries(params)
.map(([key, value]) => {
value = value === 'any' ? '' : value
return [key, value]
})
.reduce((final, item) => {
const [key, value] = item
final[key] = value
return final
}, {})
}
export const numberWithCommas = number => {
return number?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
export const getTimeStamp = () => new Date().getTime()
================================================
FILE: src/utils/index.js
================================================
export {default as polyfill} from './polyfill'
export * from './common'
================================================
FILE: src/utils/polyfill/electron/index.js
================================================
export * from './storage'
export * from './utils'
================================================
FILE: src/utils/polyfill/electron/storage.js
================================================
/**
* Saves storage with electron-store.
* This storage communicates with electron's main.js
* rather than browser's window.localStorage
*/
const {storage} = window.electron || {}
export const setStorage = (key, value) => {
try {
storage?.set(key, value)
} catch (err) {
console.log(`An error occurred when setting storage ${key} with value: `, value, err)
}
}
export const getStorage = key => {
try {
return storage?.get(key)
} catch (err) {
console.log(`An error occurred when getting storage ${key}.`, err)
return null
}
}
export const getContextFromStorage = () => {
try {
return storage?.store() || {}
} catch (err) {
console.log('An error occurred when getting context from storage.', err)
return {}
}
}
================================================
FILE: src/utils/polyfill/electron/utils.js
================================================
const {open, receive, send} = window.electron || {}
export {open, receive, send}
================================================
FILE: src/utils/polyfill/index.js
================================================
import * as web from './web'
import * as electron from './electron'
import {isElectron} from '@/config'
export default isElectron ? electron : web
================================================
FILE: src/utils/polyfill/web/index.js
================================================
export * from './storage'
export * from './utils'
================================================
FILE: src/utils/polyfill/web/storage.js
================================================
/**
* Saves storage with localStorage.
*/
const storage = window.localStorage
export const setStorage = (key, value) => {
try {
storage.setItem(key, JSON.stringify({value}))
} catch (err) {
console.log(`An error occurred when setting storage ${key} with value: `, value, err)
}
}
export const getStorage = key => {
try {
return JSON.parse(storage.getItem(key)).value
} catch (err) {
console.log(`An error occurred when getting storage ${key}.`, err)
return null
}
}
export const getContextFromStorage = () => {
try {
const context = Object.keys(storage).reduce((final, key) => {
final[key] = getStorage(key)
return final
}, {})
return context || {}
} catch (err) {
console.log('An error occurred when getting context from storage.', err)
return {}
}
}
================================================
FILE: src/utils/polyfill/web/utils.js
================================================
export const open = window.open
export const receive = () => () => {}
export const send = () => {}
================================================
FILE: webpack/chrome/webpack.config.js
================================================
/**
* The Webpack config which chrome extension project uses
*/
const path = require('path')
const webpack = require('webpack')
const {merge} = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const base = require('../webpack.base.config')
const generateChromeManifest = require('../../chrome-manifest')
generateChromeManifest()
module.exports = (_, argv) => {
console.log('webpack config argv =>', argv)
const DEV = argv.mode === 'development'
return merge(base(argv), {
/**
* The field `devtool` fixed unsafe-eval error in dev mode.
* See https://stackoverflow.com/questions/48047150/chrome-extension-compiled-by-webpack-throws-unsafe-eval-error
*/
devtool: DEV ? 'cheap-module-source-map' : false,
plugins: [
new webpack.DefinePlugin({
'process.env': {
WEBPACK_DEV: DEV,
isChrome: true,
},
}),
new HtmlWebpackPlugin({
template: path.resolve('./src/index.ejs'),
filename: 'index.html',
chunks: ['main'],
isChrome: true,
analyticsDomain: DEV ? 'raise-dev.curve.to' : 'raise-chrome.curve.to',
}),
new CopyWebpackPlugin({
patterns: [
{from: './static/chrome', to: './static'},
{from: './src/chrome/manifest.json', to: './manifest.json'},
{from: './src/chrome', to: './chrome', globOptions: {ignore: ['**/*/manifest.json']}},
],
}),
],
})
}
================================================
FILE: webpack/main/webpack.config.js
================================================
/**
* The webpack config which Electron main uses
*/
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
target: 'electron-main',
entry: {
main: path.resolve('./electron/main.js'),
preload: path.resolve('./electron/preload.js'),
},
output: {
filename: '[name].js',
path: path.resolve('./dist'),
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader',
},
{
test: /.node$/,
loader: 'node-loader',
},
],
},
resolve: {
symlinks: false,
cacheWithContext: false,
alias: {
'@shared': path.resolve('./shared'),
'@pkg': path.resolve('./package.json'),
},
},
node: {
__dirname: false,
__filename: false,
},
plugins: [
new CopyWebpackPlugin({
patterns: [{from: './static', to: './static'}],
}),
],
}
================================================
FILE: webpack/renderer/webpack.config.js
================================================
/**
* The Webpack config which Electron's renderer uses
*/
const path = require('path')
const webpack = require('webpack')
const {merge} = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin')
const base = require('../webpack.base.config')
module.exports = (_, argv) => {
console.log('webpack config argv =>', argv)
const DEV = argv.mode === 'development'
const PROD = !DEV
return merge(base(argv), {
plugins: [
new webpack.DefinePlugin({
'process.env': {
WEBPACK_DEV: DEV,
},
}),
new HtmlWebpackPlugin({
template: path.resolve('./src/index.ejs'),
filename: 'index.html',
chunks: ['main'],
isElectron: true,
analyticsDomain: DEV ? 'raise-dev.curve.to' : 'raise-desktop.curve.to',
}),
PROD && new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/^runtime.+\.js$/]),
].filter(Boolean),
})
}
================================================
FILE: webpack/webpack.base.config.js
================================================
/**
* The Webpack config which is shared by Electron's renderer and web
*/
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const terserPluginConfig = {
extractComments: false,
terserOptions: {
format: {
comments: false,
ascii_only: true,
},
compress: {
drop_console: true,
},
},
}
const splitChunksConfig = {
chunks: 'all',
cacheGroups: {
default: {
chunks: 'async',
priority: 10,
minChunks: 2,
reuseExistingChunk: true,
},
defaultVendors: false,
commons: {
chunks: 'all',
test: /[\\/]node_modules[\\/]/,
priority: 20,
minChunks: 2,
maxSize: 512 * 1024, // 512kb
name: 'commons',
filename: '[name].[chunkhash:8].js',
reuseExistingChunk: true,
},
core: {
chunks: 'all',
test: /node_modules[\\/](?:core-js|regenerator-runtime|@babel|(?:style|css)-loader)/,
priority: 30,
name: 'core',
filename: '[name].[chunkhash:8].js',
reuseExistingChunk: true,
},
react: {
chunks: 'all',
test: /node_modules[\\/](?:react|react-dom|react-router-dom)/,
priority: 100,
name: 'react',
filename: '[name].[chunkhash:8].js',
reuseExistingChunk: true,
},
},
}
module.exports = argv => {
const DEV = argv.mode === 'development'
const PROD = !DEV
return {
devtool: DEV ? 'eval-cheap-module-source-map' : false,
bail: PROD,
cache: DEV
? {type: 'memory'}
: {
type: 'filesystem',
buildDependencies: {config: [__filename]},
},
entry: ['./src/index.js'],
output: {
path: path.resolve('./dist'),
filename: `[name]${PROD ? '.[contenthash:8]' : ''}.js`,
chunkFilename: `[name]${PROD ? '.[contenthash:8]' : ''}.js`,
publicPath: PROD ? './' : '',
},
resolve: {
symlinks: false,
cacheWithContext: false,
alias: {
'@': path.resolve('./src'),
'@static': path.resolve('./static'),
'@shared': path.resolve('./shared'),
'@pkg': path.resolve('./package.json'),
},
},
devServer: {
static: path.resolve('./dist'),
port: 3000,
},
optimization: PROD
? {
runtimeChunk: 'single',
chunkIds: 'deterministic',
moduleIds: 'deterministic',
minimizer: [
new TerserPlugin(terserPluginConfig),
new CssMinimizerPlugin({test: /\.css$/}),
],
splitChunks: splitChunksConfig,
}
: undefined,
module: {
rules: [
{
test: /\.(js|jsx)$/,
include: [path.resolve('./src')],
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false,
},
},
],
},
{
test: /\.css$/,
exclude: /node_modules/,
use: [
DEV ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
exportLocalsConvention: 'camelCase',
localIdentName: '[name]__[local]___[hash:base64:5]',
},
},
},
'postcss-loader',
],
},
{
test: /\.css$/,
include: /node_modules/,
use: [DEV ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
},
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
exportLocalsConvention: 'camelCase',
localIdentName: '[name]__[local]___[hash:base64:5]',
},
},
},
'postcss-loader',
'sass-loader',
],
},
{
test: /\.(png|jpg|svg|gif)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 10kb
},
},
generator: {
filename: 'assets/images/[name].[hash:8][ext][query]',
},
},
],
},
plugins: [
PROD &&
new MiniCssExtractPlugin({
filename: 'assets/styles/[name].[contenthash:8].css',
chunkFilename: 'assets/styles/[name].[contenthash:8].css',
ignoreOrder: true,
}),
].filter(Boolean),
}
}
gitextract_6pndr1_7/
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── babel.config.js
├── build/
│ ├── after-sign-hook.js
│ ├── icon.icns
│ └── mac/
│ └── entitlements.plist
├── chrome-manifest.js
├── electron/
│ ├── common.js
│ ├── config/
│ │ ├── constants.js
│ │ ├── index.js
│ │ └── store.js
│ ├── ipc/
│ │ └── index.js
│ ├── main.js
│ ├── preload.js
│ └── update-manager.js
├── jsconfig.json
├── package.json
├── shared/
│ ├── constants.js
│ └── index.js
├── src/
│ ├── app-context.js
│ ├── app.js
│ ├── app.scss
│ ├── assets/
│ │ └── styles/
│ │ ├── global.scss
│ │ └── reset.scss
│ ├── chrome/
│ │ ├── plausible.js
│ │ └── popup.js
│ ├── components/
│ │ ├── about-modal/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── developer-content/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── filter/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── index.js
│ │ ├── raise-header/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── repository-content/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── settings-modal/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── skeleton-placeholder/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── update-notification/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ └── upper-container/
│ │ └── index.js
│ ├── config/
│ │ ├── constants.js
│ │ ├── index.js
│ │ ├── languages.js
│ │ └── spoken-languages.js
│ ├── hooks/
│ │ ├── index.js
│ │ ├── use-context-props.js
│ │ ├── use-dock-icon.js
│ │ ├── use-mode.js
│ │ ├── use-outside-click.js
│ │ └── use-scroll-position.js
│ ├── index.ejs
│ ├── index.js
│ ├── io/
│ │ ├── index.js
│ │ ├── interceptor.js
│ │ └── trending.js
│ ├── lib/
│ │ ├── index.js
│ │ └── is-electron.js
│ ├── pages/
│ │ └── index/
│ │ ├── index.js
│ │ └── styles.scss
│ └── utils/
│ ├── common.js
│ ├── index.js
│ └── polyfill/
│ ├── electron/
│ │ ├── index.js
│ │ ├── storage.js
│ │ └── utils.js
│ ├── index.js
│ └── web/
│ ├── index.js
│ ├── storage.js
│ └── utils.js
└── webpack/
├── chrome/
│ └── webpack.config.js
├── main/
│ └── webpack.config.js
├── renderer/
│ └── webpack.config.js
└── webpack.base.config.js
SYMBOL INDEX (22 symbols across 7 files)
FILE: electron/config/constants.js
constant BROWSER_WINDOW (line 4) | const BROWSER_WINDOW = {
constant MENUBAR (line 9) | const MENUBAR = {
constant INDEX_URL (line 14) | const INDEX_URL = {
constant ICON (line 21) | const ICON = {
constant INTERVAL (line 26) | const INTERVAL = {
FILE: shared/constants.js
constant IPC_FUNCTION (line 1) | const IPC_FUNCTION = {
constant STORAGE_KEY (line 10) | const STORAGE_KEY = {
FILE: src/chrome/plausible.js
function p (line 2) | function p(t){console.warn("Ignoring Event: "+t)}
function e (line 2) | function e(t,e){if(/^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/...
function d (line 2) | function d(){n!==a.pathname&&(n=a.pathname,e("pageview"))}
FILE: src/chrome/popup.js
constant MODE (line 8) | const MODE = 'mode'
FILE: src/components/filter/index.js
method reset (line 17) | reset() {
FILE: src/config/constants.js
constant VERSION (line 4) | const VERSION = pkg.version
constant TRENDING_TYPE (line 6) | const TRENDING_TYPE = {
constant MODE (line 11) | const MODE = {
constant SINCE (line 16) | const SINCE = {
constant SINCE_MAP (line 22) | const SINCE_MAP = {
constant SINCE_ARRAY (line 28) | const SINCE_ARRAY = [
constant Z_INDEX (line 34) | const Z_INDEX = {
constant URL (line 40) | const URL = {
constant ALLOWED_TIME_OF_INACTIVITY (line 47) | const ALLOWED_TIME_OF_INACTIVITY = 1000 * 60 * 60 * 3 // 3 hours
FILE: src/hooks/use-outside-click.js
function handleClickOutside (line 14) | function handleClickOutside(event) {
Condensed preview — 79 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (110K chars).
[
{
"path": ".eslintrc.js",
"chars": 1229,
"preview": "module.exports = {\n root: true,\n env: {\n browser: true,\n node: true,\n commonjs: true,\n },\n parser: '@babel/"
},
{
"path": ".gitignore",
"chars": 92,
"preview": "node_modules\ndist\nout\nsrc/chrome/manifest.json\n\n.DS_Store\n.eslintcache\n.stylelintcache\n.env\n"
},
{
"path": ".prettierrc",
"chars": 111,
"preview": "{\n\t\"arrowParens\": \"avoid\",\n\t\"bracketSpacing\": false,\n\t\"printWidth\": 100,\n\t\"semi\": false,\n\t\"singleQuote\": true\n}"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2022 Jiajun Yan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 2392,
"preview": "# Raise\n\nA simple (and unofficial) GitHub Trending client that lives in your menubar.\n\n\nconst path = require('path'"
},
{
"path": "electron/common.js",
"chars": 2771,
"preview": "import {app, BrowserWindow, Menu, shell, Tray} from 'electron'\nimport path from 'path'\nimport log from 'electron-log'\n\ni"
},
{
"path": "electron/config/constants.js",
"chars": 587,
"preview": "import path from 'path'\nimport is from 'electron-is'\n\nexport const BROWSER_WINDOW = {\n WIDTH: 1440,\n HEIGHT: 900,\n}\n\ne"
},
{
"path": "electron/config/index.js",
"chars": 52,
"preview": "export * from './constants'\nexport * from './store'\n"
},
{
"path": "electron/config/store.js",
"chars": 172,
"preview": "import Store from 'electron-store'\n\nimport {STORAGE_KEY} from '@shared'\n\nexport const store = new Store({\n defaults: {\n"
},
{
"path": "electron/ipc/index.js",
"chars": 333,
"preview": "import {app} from 'electron'\nimport {autoUpdater} from 'electron-updater'\n\nimport {isMac} from '../config'\n\nexport const"
},
{
"path": "electron/main.js",
"chars": 1787,
"preview": "import {app, ipcMain} from 'electron'\nimport {menubar} from 'menubar'\n\nimport {IPC_FUNCTION} from '@shared'\nimport pkg f"
},
{
"path": "electron/preload.js",
"chars": 1259,
"preview": "import {contextBridge, ipcRenderer, shell} from 'electron'\n\nimport {IPC_FUNCTION} from '@shared'\nimport {store} from './"
},
{
"path": "electron/update-manager.js",
"chars": 994,
"preview": "import {autoUpdater} from 'electron-updater'\nimport log from 'electron-log'\n\nimport {IPC_FUNCTION, STORAGE_KEY} from '@s"
},
{
"path": "jsconfig.json",
"chars": 282,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"paths\": {\n \"@/*\": [\"./src/*\"],\n \"@static/*\": [\"./static/*\"],"
},
{
"path": "package.json",
"chars": 4398,
"preview": "{\n \"name\": \"raise\",\n \"productName\": \"Raise\",\n \"chromeProductName\": \"Raise - GitHub Trending\",\n \"version\": \"1.2.1\",\n "
},
{
"path": "shared/constants.js",
"chars": 500,
"preview": "export const IPC_FUNCTION = {\n SHOW_ABOUT_MODAL: 'show-about-modal',\n SHOW_SETTINGS_MODAL: 'show-settings-modal',\n SH"
},
{
"path": "shared/index.js",
"chars": 28,
"preview": "export * from './constants'\n"
},
{
"path": "src/app-context.js",
"chars": 962,
"preview": "import React, {useState, useContext, useEffect} from 'react'\nimport {polyfill} from './utils'\n\nconst {setStorage} = poly"
},
{
"path": "src/app.js",
"chars": 1532,
"preview": "import React, {useState} from 'react'\nimport {Divider, Layout, Toast, Typography} from '@douyinfe/semi-ui'\n\nimport {MODE"
},
{
"path": "src/app.scss",
"chars": 242,
"preview": "::-webkit-scrollbar {\n display: none;\n}\n\n.layout {\n background-color: var(--semi-color-bg-0);\n padding: 20px;\n min-w"
},
{
"path": "src/assets/styles/global.scss",
"chars": 136,
"preview": "body {\n color: var(--semi-color-text-0);\n background-color: var(--semi-color-bg-0);\n}\n\n:global(#nprogress .bar) {\n z-"
},
{
"path": "src/assets/styles/reset.scss",
"chars": 1247,
"preview": "/* stylelint-disable */\n/* http://meyerweb.com/eric/tools/css/reset/\n v5.0.1 | 20191019\n License: none (public domai"
},
{
"path": "src/chrome/plausible.js",
"chars": 1360,
"preview": "// eslint-disable-next-line\n!function(){\"use strict\";var a=window.location,r=window.document,t=window.localStorage,o=r.c"
},
{
"path": "src/chrome/popup.js",
"chars": 676,
"preview": "/**\n * Set body's background color to either light or dark\n * This prevents popup from flashing on app launch\n */\n\nconst"
},
{
"path": "src/components/about-modal/index.js",
"chars": 1591,
"preview": "import React from 'react'\nimport {Divider, Modal, Typography} from '@douyinfe/semi-ui'\n\nimport {VERSION, Z_INDEX} from '"
},
{
"path": "src/components/about-modal/styles.scss",
"chars": 468,
"preview": ".about-modal {\n display: flex;\n flex-direction: column;\n padding: 20px 0;\n\n .logo {\n display: flex;\n flex-dire"
},
{
"path": "src/components/developer-content/index.js",
"chars": 2068,
"preview": "import React from 'react'\nimport {Typography, Layout, Card, Space} from '@douyinfe/semi-ui'\nimport {IconBranch, IconCrow"
},
{
"path": "src/components/developer-content/styles.scss",
"chars": 422,
"preview": ".content {\n padding-top: 15px;\n\n .developer {\n width: 100%;\n margin-bottom: 20px;\n background-color: var(--se"
},
{
"path": "src/components/filter/index.js",
"chars": 2739,
"preview": "import React, {forwardRef, useImperativeHandle, useRef} from 'react'\nimport {Form} from '@douyinfe/semi-ui'\n\nimport {SIN"
},
{
"path": "src/components/filter/styles.scss",
"chars": 308,
"preview": ".filter {\n display: flex;\n flex-direction: column;\n\n .bottom {\n display: flex;\n flex-direction: column;\n\n .b"
},
{
"path": "src/components/index.js",
"chars": 535,
"preview": "export {default as RaiseHeader} from './raise-header'\nexport {default as DeveloperContent} from './developer-content'\nex"
},
{
"path": "src/components/raise-header/index.js",
"chars": 4998,
"preview": "import React, {useEffect, useLayoutEffect, useRef, useState} from 'react'\nimport {Button, Collapsible, Divider, Layout, "
},
{
"path": "src/components/raise-header/styles.scss",
"chars": 1114,
"preview": ".header {\n display: flex;\n flex-direction: column;\n width: 100%;\n padding: 20px 20px 0;\n position: fixed;\n top: 0;"
},
{
"path": "src/components/repository-content/index.js",
"chars": 3604,
"preview": "import React from 'react'\nimport {Typography, Layout, Card, Space, Tooltip} from '@douyinfe/semi-ui'\nimport {IconBranch,"
},
{
"path": "src/components/repository-content/styles.scss",
"chars": 1014,
"preview": ".content {\n padding-top: 15px;\n\n .repo-header {\n display: flex;\n align-items: center;\n }\n\n .repo-author {\n "
},
{
"path": "src/components/settings-modal/index.js",
"chars": 2811,
"preview": "import React from 'react'\nimport {Button, Divider, Modal, Space, Switch, Typography} from '@douyinfe/semi-ui'\nimport {Ic"
},
{
"path": "src/components/settings-modal/styles.scss",
"chars": 307,
"preview": ".settings-modal {\n padding: 10px 0 20px;\n box-sizing: border-box;\n height: 100%;\n display: flex;\n flex-direction: c"
},
{
"path": "src/components/skeleton-placeholder/index.js",
"chars": 335,
"preview": "import React from 'react'\nimport {Skeleton} from '@douyinfe/semi-ui'\n\nimport styles from './styles.scss'\n\nconst Skeleton"
},
{
"path": "src/components/skeleton-placeholder/styles.scss",
"chars": 85,
"preview": ".skeleton {\n width: 100%;\n height: 200;\n border-radius: 6;\n margin-bottom: 20;\n}\n"
},
{
"path": "src/components/update-notification/index.js",
"chars": 1305,
"preview": "import React, {useEffect, useState} from 'react'\nimport {Banner, Button} from '@douyinfe/semi-ui'\n\nimport {IPC_FUNCTION}"
},
{
"path": "src/components/update-notification/styles.scss",
"chars": 273,
"preview": ".update {\n position: fixed;\n bottom: 0;\n left: 0;\n z-index: 99999;\n width: 100vw;\n background-color: var(--semi-co"
},
{
"path": "src/components/upper-container/index.js",
"chars": 426,
"preview": "import React, {useLayoutEffect, useState} from 'react'\n\nconst UpperContainer = ({children}) => {\n const [footerHeight, "
},
{
"path": "src/config/constants.js",
"chars": 1259,
"preview": "import pkg from '@pkg'\nimport {isElectron as checkIsElectron} from '@/lib'\n\nexport const VERSION = pkg.version\n\nexport c"
},
{
"path": "src/config/index.js",
"chars": 140,
"preview": "export {default as LANGUAGES} from './languages'\nexport {default as SPOKEN_LANGUAGES} from './spoken-languages'\nexport *"
},
{
"path": "src/config/languages.js",
"chars": 20103,
"preview": "export default [\n {urlParam: 'any', name: 'Any'},\n {urlParam: '1c-enterprise', name: '1C Enterprise'},\n {urlParam: 'a"
},
{
"path": "src/config/spoken-languages.js",
"chars": 7258,
"preview": "export default [\n {urlParam: 'any', name: 'Any'},\n {urlParam: 'ab', name: 'Abkhazian'},\n {urlParam: 'aa', name: 'Afar"
},
{
"path": "src/hooks/index.js",
"chars": 267,
"preview": "export {default as useMode} from './use-mode'\nexport {default as useDockIcon} from './use-dock-icon'\nexport {default as "
},
{
"path": "src/hooks/use-context-props.js",
"chars": 359,
"preview": "import {useContextProp} from '@/app-context'\n\nimport {STORAGE_KEY} from '@shared'\n\nexport const useTrendingType = () => "
},
{
"path": "src/hooks/use-dock-icon.js",
"chars": 422,
"preview": "import {useContextProp} from '@/app-context'\nimport {polyfill} from '@/utils'\nimport {IPC_FUNCTION, STORAGE_KEY} from '@"
},
{
"path": "src/hooks/use-mode.js",
"chars": 505,
"preview": "import {useContextProp} from '@/app-context'\nimport {MODE} from '@/config'\nimport {STORAGE_KEY} from '@shared'\n\nconst us"
},
{
"path": "src/hooks/use-outside-click.js",
"chars": 780,
"preview": "import {useEffect} from 'react'\n\n/**\n * Executes a handler function on click outside of a specific div\n * See: https://s"
},
{
"path": "src/hooks/use-scroll-position.js",
"chars": 201,
"preview": "import {useScroll} from 'ahooks'\n\nconst useScrollPosition = () => {\n const scrollRef = useScroll()\n const scrollPositi"
},
{
"path": "src/index.ejs",
"chars": 871,
"preview": "<html lang=\"en\">\n\n\t<head>\n\t\t<meta charset=\"UTF-8\">\n\t\t<meta\n\t\t\thttp-equiv=\"X-UA-Compatible\"\n\t\t\tcontent=\"IE=edge\"\n\t\t>\n\t\t<m"
},
{
"path": "src/index.js",
"chars": 202,
"preview": "import React from 'react'\nimport {createRoot} from 'react-dom/client'\n\nimport App from './app'\n\nconst container = docume"
},
{
"path": "src/io/index.js",
"chars": 27,
"preview": "export * from './trending'\n"
},
{
"path": "src/io/interceptor.js",
"chars": 460,
"preview": "import axios from 'axios'\nimport NProgress from 'nprogress'\n\nNProgress.configure({showSpinner: false})\n\naxios.intercepto"
},
{
"path": "src/io/trending.js",
"chars": 1428,
"preview": "import {snakeCase} from 'lodash'\n\nimport axios from './interceptor'\nimport {TRENDING_TYPE, URL} from '@/config'\nimport {"
},
{
"path": "src/lib/index.js",
"chars": 52,
"preview": "export {default as isElectron} from './is-electron'\n"
},
{
"path": "src/lib/is-electron.js",
"chars": 655,
"preview": "const isElectron = () => {\n // Renderer process\n if (\n typeof window !== 'undefined' &&\n typeof window.process ="
},
{
"path": "src/pages/index/index.js",
"chars": 3702,
"preview": "import React, {useEffect, useState} from 'react'\nimport {Typography, BackTop, Toast, Empty} from '@douyinfe/semi-ui'\nimp"
},
{
"path": "src/pages/index/styles.scss",
"chars": 415,
"preview": ".back-top {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 30px;\n width: 30px;\n border-r"
},
{
"path": "src/utils/common.js",
"chars": 621,
"preview": "export const truncate = (str, maxLength = 14) => {\n return str.length > maxLength ? `${str.substring(0, maxLength)}...`"
},
{
"path": "src/utils/index.js",
"chars": 72,
"preview": "export {default as polyfill} from './polyfill'\nexport * from './common'\n"
},
{
"path": "src/utils/polyfill/electron/index.js",
"chars": 50,
"preview": "export * from './storage'\nexport * from './utils'\n"
},
{
"path": "src/utils/polyfill/electron/storage.js",
"chars": 768,
"preview": "/**\n * Saves storage with electron-store.\n * This storage communicates with electron's main.js\n * rather than browser's "
},
{
"path": "src/utils/polyfill/electron/utils.js",
"chars": 82,
"preview": "const {open, receive, send} = window.electron || {}\n\nexport {open, receive, send}\n"
},
{
"path": "src/utils/polyfill/index.js",
"chars": 148,
"preview": "import * as web from './web'\nimport * as electron from './electron'\nimport {isElectron} from '@/config'\n\nexport default "
},
{
"path": "src/utils/polyfill/web/index.js",
"chars": 50,
"preview": "export * from './storage'\nexport * from './utils'\n"
},
{
"path": "src/utils/polyfill/web/storage.js",
"chars": 829,
"preview": "/**\n * Saves storage with localStorage.\n */\n\nconst storage = window.localStorage\n\nexport const setStorage = (key, value)"
},
{
"path": "src/utils/polyfill/web/utils.js",
"chars": 101,
"preview": "export const open = window.open\n\nexport const receive = () => () => {}\n\nexport const send = () => {}\n"
},
{
"path": "webpack/chrome/webpack.config.js",
"chars": 1534,
"preview": "/**\n * The Webpack config which chrome extension project uses\n */\n\nconst path = require('path')\nconst webpack = require("
},
{
"path": "webpack/main/webpack.config.js",
"chars": 939,
"preview": "/**\n * The webpack config which Electron main uses\n */\n\nconst path = require('path')\nconst CopyWebpackPlugin = require('"
},
{
"path": "webpack/renderer/webpack.config.js",
"chars": 1011,
"preview": "/**\n * The Webpack config which Electron's renderer uses\n */\n\nconst path = require('path')\nconst webpack = require('webp"
},
{
"path": "webpack/webpack.base.config.js",
"chars": 4762,
"preview": "/**\n * The Webpack config which is shared by Electron's renderer and web\n */\n\nconst path = require('path')\nconst MiniCss"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the meetyan/raise GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 79 files (98.7 KB), approximately 30.8k tokens, and a symbol index with 22 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.