├── .gitignore ├── Procfile ├── README.md ├── app.json ├── package-lock.json ├── package.json ├── public ├── .DS_Store ├── assets │ ├── .DS_Store │ ├── circular-book.woff │ ├── circular-medium.woff │ ├── cover.png │ ├── dark-favicon.png │ ├── light-favicon.png │ └── logo.png ├── index.html └── manifest.json ├── readmepic.png ├── server.js ├── setupProxy └── src ├── .DS_Store ├── App.js ├── assets ├── .DS_Store ├── lightLogo.png └── logo.png ├── components ├── Nav.js ├── Repo.js └── checkTheme.js ├── history.js ├── index.js ├── setupProxy.js ├── styles └── app.scss └── views ├── Dashboard.js └── Splash.js /.gitignore: -------------------------------------------------------------------------------- 1 | # GENERATED BY Gitgat 2 | 3 | # .gitignore file for node 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | .parcel-cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and not Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | 111 | # Stores VSCode versions used for testing VSCode extensions 112 | .vscode-test 113 | 114 | # yarn v2 115 | 116 | .yarn/cache 117 | .yarn/unplugged 118 | .yarn/build-state.yml 119 | .pnp.* 120 | 121 | 122 | .env 123 | build/* 124 | .DS_Store -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitCleanup 2 | 3 | ### Clean up your GitHub profile by deleting abandoned or empty repositories with just a few clicks. 4 | 5 | A web app built with React + Express that lets you bulk delete repositories with just a few clicks. 6 | 7 | [![GitCleanup](https://raw.githubusercontent.com/MehediH/GitCleanup/master/readmepic.png)](https://www.gitcleanup.com) 8 | 9 | You can access the app on [gitcleanup.com](https://www.gitcleanup.com), or deploy your own instance on Heroku: 10 | 11 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/MehediH/GitCleanup) 12 | 13 | To run the app locally, you'll need to set your GitHub OAuth app credentials as environment variables (`GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`). Then run `npm run dev` to start the Express server and the React app. -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitCleanup", 3 | "description": "Say hello to GitHub Zero: de-clutter your GitHub profile and get rid of unused repositories with just a few clicks.", 4 | "repository": "https://github.com/MehediH/GitCleanup", 5 | "logo": "https://github.com/MehediH/GitCleanup/raw/master/src/assets/lightLogo.png", 6 | "keywords": ["github", "git", "node", "express", "react"], 7 | "env": { 8 | "GITHUB_CLIENT_ID": { 9 | "description": "Client ID for your GitHub OAuth app. You can make one at https://github.com/settings/developers" 10 | }, 11 | "GITHUB_CLIENT_SECRET": { 12 | "description": "Client secret for the GitHub OAuth app." 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-cleanup", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "body-parser": "^1.19.0", 7 | "compression": "^1.7.4", 8 | "concurrently": "^5.1.0", 9 | "connect-ensure-login": "^0.1.1", 10 | "cookie-parser": "^1.4.5", 11 | "cors": "^2.8.5", 12 | "dotenv": "^8.2.0", 13 | "express": "^4.17.1", 14 | "express-session": "^1.17.0", 15 | "favicon-switcher": "^1.2.2", 16 | "fuse.js": "^5.1.0", 17 | "heroku-ssl-redirect": "0.0.4", 18 | "http-proxy-middleware": "^1.0.3", 19 | "morgan": "^1.10.0", 20 | "node-sass": "^4.13.1", 21 | "nodemon": "^2.0.2", 22 | "passport": "^0.4.1", 23 | "passport-github": "^1.1.0", 24 | "react": "^16.8.6", 25 | "react-dom": "^16.8.6", 26 | "react-icons": "^3.9.0", 27 | "react-router-dom": "^5.1.2", 28 | "react-scripts": "3.0.1", 29 | "react-spinners": "^0.8.1", 30 | "styled-components": "^5.0.1" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "test": "react-scripts test", 36 | "eject": "react-scripts eject", 37 | "server": "nodemon server.js", 38 | "dev": "concurrently \"npm start\" \"npm run server\" " 39 | }, 40 | "proxy": "http://localhost:5000", 41 | "eslintConfig": { 42 | "extends": "react-app" 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/public/.DS_Store -------------------------------------------------------------------------------- /public/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/public/assets/.DS_Store -------------------------------------------------------------------------------- /public/assets/circular-book.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/public/assets/circular-book.woff -------------------------------------------------------------------------------- /public/assets/circular-medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/public/assets/circular-medium.woff -------------------------------------------------------------------------------- /public/assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/public/assets/cover.png -------------------------------------------------------------------------------- /public/assets/dark-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/public/assets/dark-favicon.png -------------------------------------------------------------------------------- /public/assets/light-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/public/assets/light-favicon.png -------------------------------------------------------------------------------- /public/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/public/assets/logo.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | GitCleanup 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "GitCleanup", 3 | "name": "GitCleanup", 4 | "icons": [ 5 | { 6 | "src": "assets/logo.png", 7 | "sizes": "64x64 32x32 24x24 16x16" 8 | } 9 | ], 10 | "start_url": ".", 11 | "display": "standalone", 12 | "theme_color": "#000000", 13 | "background_color": "#ffffff" 14 | } 15 | -------------------------------------------------------------------------------- /readmepic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/readmepic.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const express = require("express"); 4 | const cors = require("cors"); 5 | const passport = require("passport"); 6 | const GitHubStrategy = require("passport-github").Strategy; 7 | const request = require("request"); 8 | const connect = require("connect-ensure-login"); 9 | const compression = require("compression"); 10 | const path = require("path"); 11 | const sslRedirect = require('heroku-ssl-redirect'); 12 | 13 | let loginUrl = "/"; // default redirect path when auth fails 14 | 15 | passport.serializeUser((user, cb) => { 16 | cb(null, user); 17 | }) 18 | 19 | passport.deserializeUser((user, cb) => { 20 | cb(null, user); 21 | }) 22 | 23 | // GitHub Strategy for login 24 | passport.use(new GitHubStrategy({ 25 | clientID: process.env.GITHUB_CLIENT_ID, 26 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 27 | scope: "repo,delete_repo,user" // we need to view repo details and have permission to delete 28 | }, (accessToken, refreshToken, profile, cb) => { 29 | let user = { 30 | ...profile, 31 | accessToken 32 | }; 33 | return cb(null, user); 34 | } 35 | )); 36 | 37 | const app = express(); 38 | const dev = app.get("env") !== "production"; 39 | 40 | // Use application-level middleware for common functionality, including 41 | // logging, parsing, and session handling. 42 | app.use(require('body-parser').urlencoded({ extended: true })); 43 | app.use(require('express-session')({ secret: 'totoro', resave: true, saveUninitialized: true })); 44 | app.use(cors()); 45 | app.use(sslRedirect()); 46 | 47 | if(!dev){ 48 | app.disable("x-powered-by") 49 | app.use(compression()) 50 | app.use(require('morgan')('tiny')); 51 | 52 | app.use(express.static(path.resolve(__dirname, "build"))) 53 | 54 | } else{ 55 | app.use(require('morgan')('dev')); 56 | } 57 | 58 | // Initialize Passport and restore authentication state, if any, from the 59 | // session. 60 | app.use(passport.initialize()); 61 | app.use(passport.session()); 62 | 63 | app.get("/api/login", passport.authenticate("github")); 64 | 65 | app.get("/api/callback", passport.authenticate("github", {failureRedirect: loginUrl}), (req, res) => { 66 | res.redirect("/") 67 | }); 68 | 69 | app.get("/api/logout", (req, res) => { 70 | req.logout(); 71 | res.redirect("/"); 72 | }); 73 | 74 | // Retrusn user details 75 | app.get("/api/user", connect.ensureLoggedIn(loginUrl), (req, res) => { 76 | res.send(req.user); 77 | }); 78 | 79 | 80 | // Get user's repository details from GitHub API and return 81 | app.get("/api/repos", connect.ensureLoggedIn(loginUrl), (req, res) => { 82 | let user = req.user; 83 | 84 | let accessToken = user["accessToken"]; 85 | 86 | let repos = [] 87 | let pages = [1] 88 | 89 | let repoCount = user["_json"]["public_repos"] + user["_json"]["total_private_repos"]; 90 | 91 | while(repoCount > 100){ 92 | pages.push(pages[pages.length-1] + 1); 93 | repoCount -= 100; 94 | } 95 | 96 | let getAPIPages = pages.map((page) => { 97 | return new Promise((resolve, reject) => { 98 | console.log('https://api.github.com/user/repos?sort=created&per_page=100&page=' + page) 99 | request({ 100 | url: 'https://api.github.com/user/repos?sort=created&per_page=100&page=' + page, 101 | headers: { 102 | "Authorization": `token ${accessToken}`, 103 | "User-Agent": "GitCleanup" 104 | } 105 | }, function(err, res) { 106 | if(!err){ 107 | let data = Object.values(JSON.parse(res.body)); 108 | repos.push(...data) 109 | 110 | resolve(repos) 111 | } 112 | }) 113 | }) 114 | 115 | }) 116 | 117 | Promise.all(getAPIPages).then((data) => { 118 | res.json([].concat.apply([], repos)) 119 | }) 120 | 121 | }) 122 | 123 | // Given a repo name, we can delete the repository 124 | app.get("/api/repos/delete/:fullname", connect.ensureLoggedIn(loginUrl), (req, res) => { 125 | let user = req.user; 126 | let accessToken = user["accessToken"]; 127 | 128 | return new Promise(resolve => { 129 | request({ 130 | url: `https://api.github.com/repos/${req.params.fullname}`, 131 | headers: { 132 | "Authorization": `token ${accessToken}`, 133 | "User-Agent": "GitCleanup" 134 | }, 135 | method: "DELETE" 136 | }, function(err, res) { 137 | if(!err){ 138 | resolve(res) 139 | } 140 | }); 141 | }).then(out => { 142 | if(out.statusCode === 204 ){ 143 | res.json({ 144 | statusCode: out.statusCode, 145 | message: `Successfully deleted your repository: ${req.params.id}` 146 | }) 147 | } else{ 148 | res.json({ 149 | statusCode: out.statusCode, 150 | message: `Something went wrong: ${out.statusMessage}` 151 | }) 152 | } 153 | }) 154 | }) 155 | 156 | 157 | 158 | if(!dev){ 159 | app.get("*", (req, res) => { 160 | res.sendFile(path.resolve(__dirname, "build", "index.html")); 161 | }) 162 | } 163 | 164 | 165 | const PORT = process.env.PORT || 5000; 166 | app.listen(PORT); -------------------------------------------------------------------------------- /setupProxy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/setupProxy -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/src/.DS_Store -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Router, Route } from "react-router-dom"; 3 | import { createGlobalStyle } from 'styled-components'; 4 | 5 | import history from "./history"; 6 | 7 | import Splash from "./views/Splash"; 8 | import Dashboard from "./views/Dashboard"; 9 | 10 | import initSwitcher from "favicon-switcher"; 11 | 12 | import "./styles/app.scss"; 13 | import {checkTheme} from "./components/checkTheme" 14 | 15 | let GlobalStyle = createGlobalStyle` 16 | @font-face { 17 | font-family: 'Circular'; 18 | src: url('./assets/circular-book.woff') format('woff'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | @font-face { 23 | font-family: 'Circular'; 24 | src: url('./assets/circular-medium.woff') format('woff'); 25 | font-weight: 600; 26 | font-style: normal; 27 | } 28 | html, 29 | body { 30 | height: 100%; 31 | width: 100%; 32 | } 33 | 34 | body { 35 | font-family: 'Circular', Helvetica, Arial, sans-serif; 36 | } 37 | ` 38 | 39 | class App extends Component { 40 | constructor(props){ 41 | super(props) 42 | 43 | this.state = { 44 | user: {} 45 | } 46 | 47 | initSwitcher(); 48 | 49 | } 50 | 51 | componentWillMount(){ 52 | fetch("/api/user/").then(res => res.json()).then((res) => { 53 | this.setState({user: res}) 54 | }).catch(err => console.error(err)) 55 | }j 56 | 57 | 58 | render() { 59 | let isLight = checkTheme(); 60 | let {user} = this.state; 61 | 62 | return ( 63 | 64 | 65 | { 66 | user.accessToken ? 67 | }/> 68 | : }/> 69 | } 70 | 71 | 75 | 76 | ); 77 | } 78 | } 79 | 80 | export default App; -------------------------------------------------------------------------------- /src/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/src/assets/.DS_Store -------------------------------------------------------------------------------- /src/assets/lightLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/src/assets/lightLogo.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MehediH/GitCleanup/193bc16cfb90fb04f27a623280e14d0125005eb0/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FiLogOut, FiSearch, FiSun, FiMoon, FiCoffee, FiMenu, FiX} from "react-icons/fi"; 3 | import logo from "../assets/logo.png"; 4 | import lightLogo from "../assets/lightLogo.png"; 5 | import Fuse from "fuse.js"; 6 | import Repo from './Repo'; 7 | 8 | class Nav extends Component { 9 | constructor(props){ 10 | super(props) 11 | 12 | this.state = { 13 | user: {}, 14 | search: false, 15 | searchResults: [], 16 | searchQuery: "", 17 | mobileMenuOpen: false, 18 | searchBoxX: 0, 19 | searchBoxWidth: 0, 20 | resized: false 21 | } 22 | 23 | this.searchBox = React.createRef(); 24 | 25 | window.addEventListener("resize", () => { 26 | this.resetSearchBoxPosition(false) 27 | }) 28 | } 29 | 30 | componentWillMount(){ 31 | fetch("/api/user").then(res => res.json()).then((res) => { 32 | this.setState({user: res}) 33 | }).catch(err => console.log(err)) 34 | } 35 | 36 | resetSearchBoxPosition(resized=true){ 37 | this.setState({ 38 | searchBoxX: this.searchBox.current.getBoundingClientRect().x, 39 | searchBoxWidth: this.searchBox.current.getBoundingClientRect().width, 40 | resized 41 | }) 42 | } 43 | 44 | performSearch(query){ 45 | if(!this.state.resized){ 46 | this.resetSearchBoxPosition() 47 | } 48 | 49 | this.setState({searchQuery: query}) 50 | 51 | const options = { 52 | threshold: 0.5, 53 | keys: ['name', 'description', 'url', 'language'] 54 | } 55 | 56 | let {repos} = this.props; 57 | 58 | let fuse = new Fuse(repos, options) 59 | 60 | let result = fuse.search(query); 61 | 62 | this.setState({searchResults: result}) 63 | } 64 | 65 | switchTheme(){ 66 | let isLight = document.body.classList.contains("light") ? true : false; 67 | 68 | if(isLight){ 69 | document.body.classList.replace("light", "dark") 70 | localStorage.setItem("gcTheme", "dark") 71 | } else{ 72 | document.body.classList.replace("dark", "light") 73 | localStorage.setItem("gcTheme", "light") 74 | } 75 | 76 | 77 | this.setState({isLight: !this.state.isLight}) 78 | } 79 | 80 | render() { 81 | const {user} = this.state; 82 | 83 | return ( 84 | 85 |
86 |
87 | GitCleanup logo 88 | GitCleanup logo 89 | { 90 | this.setState({mobileMenuOpen: false, search: false, searchResults: []}) 91 | document.body.classList.remove("mm-open") 92 | }}/> 93 |
94 | 133 | { 134 | this.setState({mobileMenuOpen: true}) 135 | document.body.classList.add("mm-open") 136 | }}/> 137 |
138 | { 139 | (this.state.searchResults.length !== 0) && 140 | 141 | 142 |
143 |
144 |
    145 | {this.state.searchResults.map((repo) => { 146 | repo = repo.item; 147 | 148 | return { 149 | this.props.addToDelete(repo) 150 | this.setState({search: false, searchResults: []}) 151 | }} {...repo}> 152 | })} 153 |
154 |
155 |
156 |
this.setState({search: false, searchResults: []})}>
157 |
158 | } 159 |
160 | ); 161 | } 162 | } 163 | 164 | export default Nav; -------------------------------------------------------------------------------- /src/components/Repo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FiTrash, FiStar, FiEye, FiGitMerge, FiCornerUpLeft, FiGlobe, FiLock } from "react-icons/fi"; 3 | import { GoMarkGithub } from "react-icons/go"; 4 | 5 | class Repo extends Component { 6 | render() { 7 | let repo = this.props; 8 | 9 | return ( 10 |
  • 11 |
    12 | 13 | {repo.permissions.admin ?repo.name : repo.full_name} 14 | 15 |
    16 | { repo.description &&

    {repo.description}

    } 17 | 37 | 48 | { repo.delete ? this.props.onClick(e)}>Don't Delete : this.props.onClick(e)}>Add to Delete} 49 |
  • 50 | ); 51 | } 52 | } 53 | 54 | export default Repo; -------------------------------------------------------------------------------- /src/components/checkTheme.js: -------------------------------------------------------------------------------- 1 | export let checkTheme = () => { 2 | let isLight = false; 3 | 4 | if(localStorage.getItem("gcTheme") !== undefined){ 5 | isLight = localStorage.getItem("gcTheme") === "light" ? true : false 6 | } 7 | 8 | if(window.matchMedia){ 9 | const themeChecker = window.matchMedia('(prefers-color-scheme: light)'); 10 | 11 | if(themeChecker.matches || isLight){ 12 | document.body.classList.replace("dark", "light"); 13 | isLight = true; 14 | } 15 | 16 | themeChecker.addListener( () => { 17 | if(themeChecker.matches){ 18 | document.body.classList.replace("dark", "light"); 19 | isLight = true; 20 | } else{ 21 | document.body.classList.replace("light", "dark"); 22 | isLight = false 23 | } 24 | }) 25 | } else{ 26 | if(isLight){ 27 | document.body.classList.replace("dark", "light"); 28 | isLight = true; 29 | } 30 | } 31 | 32 | 33 | return isLight; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from "history"; 2 | 3 | export default createBrowserHistory(); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('root') 8 | ); -------------------------------------------------------------------------------- /src/setupProxy.js: -------------------------------------------------------------------------------- 1 | const { createProxyMiddleware } = require('http-proxy-middleware'); 2 | 3 | module.exports = function(app) { 4 | app.use( 5 | '/api', 6 | createProxyMiddleware({ 7 | target: 'http://localhost:5000/' 8 | }) 9 | ); 10 | }; -------------------------------------------------------------------------------- /src/styles/app.scss: -------------------------------------------------------------------------------- 1 | .dark{ 2 | --bg: rgb(13, 6, 32); 3 | --innerElem: rgb(36, 36, 36); 4 | --cardBg: #333; 5 | --text: #fff; 6 | --boxShadow: 0px 0px 25px rgba(0, 0, 0, 0.482); 7 | --lightSadow: 0px 0px 3px rgba(0, 0, 0, 0.35); 8 | 9 | .lightLogo{ 10 | display: none; 11 | } 12 | } 13 | 14 | .light{ 15 | --bg: #f5f5f5; 16 | --innerElem: #e9eaed; 17 | --cardBg: #fff; 18 | --text: #333; 19 | --boxShadow: 0px 0px 25px rgba(218, 209, 209, 0.55); 20 | --lightSadow: 0px 0px 3px rgba(210, 193, 193, 0.35); 21 | 22 | .darkLogo{ 23 | display: none; 24 | } 25 | } 26 | 27 | html, body { 28 | height: auto !important; 29 | width: auto !important; 30 | } 31 | 32 | 33 | body{ 34 | background-color: var(--bg); 35 | color: var(--text); 36 | background-image: url("https://desktop.github.com/images/star-bg.svg"); 37 | background-repeat: repeat-x; 38 | background-position: right; 39 | max-width: 1500px; 40 | margin: 0 auto; 41 | padding: 0 50px; 42 | animation: mte 1.5s 0s forwards ease-in-out; 43 | transition: background-color 100ms ease-in-out, color 200ms ease-in-out !important; 44 | overflow-x: hidden; 45 | } 46 | 47 | 48 | @keyframes mte { 49 | 0% { 50 | background-position: right; 51 | } 52 | 53 | 100%{ 54 | background-position: center 0, 0 0, 0 0; 55 | } 56 | } 57 | 58 | @keyframes dgb { 59 | 0% { 60 | opacity: 0; 61 | display: none; 62 | transform: translateY(50px); 63 | } 64 | 65 | 100%{ 66 | opacity: 1; 67 | display: block; 68 | transform: translateY(0px); 69 | } 70 | } 71 | 72 | @keyframes tgb { 73 | 0% { 74 | opacity: 0; 75 | } 76 | 77 | 100%{ 78 | opacity: 0.75; 79 | } 80 | } 81 | 82 | 83 | .welcome{ 84 | text-align: center; 85 | border-radius: 50px; 86 | padding: 100px; 87 | position: absolute; 88 | left: 50%; 89 | top: 50%; 90 | transform: translate(-50%, -50%); 91 | 92 | .logo-cont{ 93 | opacity: 0; 94 | animation: dgb 1s 0.8s forwards ease-in-out; 95 | } 96 | 97 | img{ 98 | width: 120px; 99 | margin: 0 auto; 100 | display: block; 101 | 102 | } 103 | 104 | h1{ 105 | font-size: 50px; 106 | opacity: 0; 107 | animation: dgb 1s 1s forwards ease-in-out; 108 | } 109 | 110 | h2{ 111 | font-size: 25px; 112 | opacity: 0.86; 113 | font-weight: 400; 114 | opacity: 0; 115 | animation: dgb 1s 1.2s forwards ease-in-out; 116 | } 117 | 118 | a{ 119 | text-decoration: none; 120 | background-color: var(--innerElem); 121 | padding: 0 20px; 122 | height: 50px; 123 | display: inline-block; 124 | border-radius: 50px; 125 | line-height: 50px; 126 | color: var(--text); 127 | transition: 0.3s background ease-in-out; 128 | margin-top: 10px; 129 | opacity: 0; 130 | animation: dgb 1s 1.4s forwards ease-in-out; 131 | 132 | &:hover, &:active, &:focus{ 133 | transform: translateY(-3px); 134 | background-color: var(--cardBg); 135 | } 136 | 137 | svg{ 138 | font-size: 20px; 139 | padding-right: 15px; 140 | padding-top: 15px; 141 | display: block; 142 | float: left; 143 | } 144 | } 145 | } 146 | 147 | .search-box ul::-webkit-scrollbar, .group-list::-webkit-scrollbar, .modal .inner::-webkit-scrollbar, .nav-items::-webkit-scrollbar{ 148 | width: 12px; 149 | } 150 | 151 | .search-box ul::-webkit-scrollbar-thumb, .group-list::-webkit-scrollbar-thumb, .modal .inner::-webkit-scrollbar-thumb{ 152 | border-radius: 10px; 153 | background-color: var(--innerElem); 154 | cursor: pointer; 155 | } 156 | 157 | .nav-items::-webkit-scrollbar-thumb { 158 | border-radius: 10px; 159 | background-color: var(--cardBg); 160 | cursor: pointer; 161 | } 162 | 163 | 164 | .search-box ul::-webkit-scrollbar-thumb:hover, .group-list::-webkit-scrollbar-thumb:hover, .modal .inner::-webkit-scrollbar-thumb:hover, .nav-items::-webkit-scrollbar-thumb:hover{ 165 | border-radius: 10px; 166 | background-color: rgb(31, 30, 30); 167 | cursor: pointer; 168 | } 169 | 170 | @keyframes tom { 171 | 0%{ 172 | border-radius: 50px; 173 | } 174 | 175 | 100%{ 176 | border-radius: 50px 50px 0px 0px; 177 | } 178 | } 179 | 180 | .dashboard{ 181 | color: var(--text); 182 | 183 | .backdrop{ 184 | position: fixed; 185 | width: 100%; 186 | height: 100%; 187 | z-index: 88; 188 | left: 0; 189 | top: 0; 190 | } 191 | 192 | .search-box{ 193 | position: absolute; 194 | right: 230px; 195 | top: 115px; 196 | z-index: 9999; 197 | height: 500px; 198 | background-color: var(--cardBg); 199 | border-radius: 0px 0px 50px 50px; 200 | box-shadow: var(--boxShadow); 201 | display: block !important; 202 | overflow: hidden; 203 | opacity: 0; 204 | animation: dgb 0.3s 0.1s forwards cubic-bezier(0.68, -0.55, 0.265, 1.55); 205 | 206 | 207 | 208 | .inner{ 209 | overflow: hidden; 210 | padding: 25px 20px 0px 20px !important; 211 | 212 | .results{ 213 | margin-bottom: 0px !important; 214 | height: 460px; 215 | } 216 | } 217 | } 218 | 219 | header{ 220 | margin: 50px 0; 221 | display: flex; 222 | justify-content: space-between; 223 | opacity: 0; 224 | animation: dgb 0.5s 0.1s forwards cubic-bezier(0.68, -0.55, 0.265, 1.55); 225 | 226 | .logo{ 227 | width: 50px; 228 | } 229 | 230 | .search-open{ 231 | grid-template-columns: 2.5fr 65px 0.5fr 1fr 65px !important; 232 | 233 | .search:hover{ 234 | transform: none !important; 235 | } 236 | 237 | input{ 238 | width: 100%; 239 | background-color: transparent; 240 | border: 0px; 241 | font-size: 20px; 242 | font-size: 18px; 243 | color: var(--text); 244 | outline: 0; 245 | } 246 | } 247 | 248 | .results-showing{ 249 | .search{ 250 | border-radius: 50px; 251 | animation: tom 0.3s 0.1s forwards cubic-bezier(0.68, -0.55, 0.265, 1.55); 252 | } 253 | } 254 | 255 | .nav-items{ 256 | list-style-type: none; 257 | padding: 0px; 258 | margin: 0px; 259 | display: grid; 260 | grid-template-columns: 1fr 65px 0.5fr 1fr 65px; 261 | grid-gap: 20px; 262 | 263 | a{ 264 | text-decoration: none; 265 | color: var(--text); 266 | 267 | } 268 | 269 | .nav-item{ 270 | background-color: var(--cardBg); 271 | color: var(--text); 272 | padding: 0px 20px; 273 | border-radius: 50px; 274 | font-size: 20px; 275 | box-shadow: var(--boxShadow); 276 | display: flex; 277 | justify-content: flex-start; 278 | height: 65px; 279 | line-height: 65px; 280 | transition: 0.3s transform ease-in-out; 281 | position: relative; 282 | cursor: pointer; 283 | 284 | &:hover, &:active, &:focus{ 285 | transform: translateY(-3px); 286 | } 287 | 288 | .mm{ 289 | display: none; 290 | } 291 | 292 | img{ 293 | width: 35px; 294 | height: 35px; 295 | border-radius: 50px; 296 | margin-top: 15px; 297 | } 298 | 299 | svg{ 300 | font-size: 25px; 301 | margin-top: 20px; 302 | 303 | } 304 | 305 | span, input{ 306 | margin-left: 15px; 307 | } 308 | 309 | 310 | } 311 | } 312 | } 313 | 314 | .repo-list{ 315 | display: grid; 316 | grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); 317 | grid-gap: 50px; 318 | 319 | .public{ 320 | animation: dgb 0.5s 0.5s forwards cubic-bezier(0.68, -0.55, 0.265, 1.55); 321 | } 322 | 323 | .private{ 324 | animation: dgb 0.5s 0.7s forwards cubic-bezier(0.68, -0.55, 0.265, 1.55); 325 | } 326 | 327 | .delete{ 328 | animation: dgb 0.5s 0.9s forwards cubic-bezier(0.68, -0.55, 0.265, 1.55); 329 | } 330 | 331 | ul{ 332 | list-style-type: none; 333 | padding: 0px; 334 | overflow-y: scroll; 335 | height: calc(100% - 50px); 336 | padding-right: 10px; 337 | 338 | .repo{ 339 | background-color: var(--innerElem); 340 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.35); 341 | padding: 25px; 342 | margin-bottom: 10px; 343 | border-radius: 10px; 344 | transition: 0.3s all ease-in-out; 345 | overflow: visible; 346 | 347 | .header{ 348 | line-height: 35px; 349 | 350 | .name{ 351 | font-size: 17px; 352 | color: var(--text); 353 | text-decoration: none; 354 | transition: 0.3s all ease-in-out; 355 | 356 | &:hover{ 357 | opacity: 0.85; 358 | } 359 | } 360 | 361 | svg{ 362 | font-size: 20px; 363 | } 364 | 365 | 366 | } 367 | 368 | .btn{ 369 | background-color: var(--cardBg); 370 | padding: 10px 15px; 371 | margin-top: 15px; 372 | display: inline-block; 373 | text-transform: uppercase; 374 | font-size: 12px; 375 | border-radius: 50px; 376 | cursor: pointer; 377 | transition: 0.3s transform ease-in-out, 0.3s box-shadow ease-in-out; 378 | box-shadow: var(--lightSadow); 379 | 380 | &:hover{ 381 | transform: translateY(-2px); 382 | box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.32); 383 | } 384 | 385 | svg{ 386 | font-size: 12px; 387 | padding-right: 10px; 388 | } 389 | } 390 | 391 | p{ 392 | font-weight: 300; 393 | padding-top: 15px; 394 | font-size: 14.5px; 395 | margin: 0px; 396 | } 397 | 398 | .stats{ 399 | display: flex; 400 | justify-content: space-between; 401 | overflow: hidden; 402 | padding: 15px 0; 403 | 404 | a{ 405 | text-decoration: none; 406 | color: var(--text); 407 | transition: 0.3s transform ease-in-out; 408 | 409 | &:hover, &:active, &:focus{ 410 | transform: translateY(-2px); 411 | } 412 | } 413 | 414 | li{ 415 | background-color: var(--cardBg); 416 | box-shadow: var(--lightSadow); 417 | padding: 10px 15px; 418 | border-radius: 50px; 419 | display: flex; 420 | 421 | svg{ 422 | padding-right: 10px; 423 | padding-top: 2px; 424 | } 425 | } 426 | } 427 | 428 | a.gh-link{ 429 | text-decoration: none; 430 | color: var(--text); 431 | font-weight: 400; 432 | opacity: 0.75; 433 | transition: 0.3s opacity ease-in-out; 434 | 435 | &:hover{ 436 | opacity: 1; 437 | } 438 | 439 | svg{ 440 | padding-right: 10px; 441 | } 442 | } 443 | 444 | footer{ 445 | display: flex; 446 | justify-content: space-between; 447 | padding-top: 10px; 448 | } 449 | } 450 | } 451 | 452 | .group{ 453 | background-color: var(--cardBg); 454 | box-shadow: var(--boxShadow); 455 | border-radius: 50px; 456 | padding: 30px 50px 0px 50px; 457 | height: 67vh; 458 | position: relative; 459 | overflow: hidden; 460 | opacity: 0; 461 | 462 | .loader{ 463 | position: absolute; 464 | top: 50%; 465 | left: 50%; 466 | transform: translate(-50%, -50%); 467 | } 468 | 469 | h3{ 470 | text-transform: uppercase; 471 | 472 | svg{ 473 | float: right; 474 | font-size: 23px; 475 | opacity: 0.5; 476 | } 477 | } 478 | 479 | .notice{ 480 | font-size: 18px; 481 | line-height: 28px; 482 | opacity: 0.85; 483 | } 484 | 485 | .delete-list{ 486 | height: calc(100% - 83px); 487 | } 488 | 489 | 490 | 491 | .delete-button{ 492 | bottom: 0; 493 | position: absolute; 494 | width: 100%; 495 | left: 0; 496 | height: 50px; 497 | background-color: #7f0505; 498 | border: 0px; 499 | outline: 0px; 500 | color: #fff; 501 | text-transform: uppercase; 502 | font-size: 15px; 503 | box-shadow: var(--boxShadow); 504 | transition: 0.3s background-color ease-in-out; 505 | cursor: pointer; 506 | 507 | &:hover{ 508 | background-color: #710505; 509 | } 510 | 511 | svg{ 512 | padding-right: 15px; 513 | } 514 | } 515 | } 516 | } 517 | 518 | .overlay{ 519 | position: fixed; 520 | width: 100%; 521 | height: 100%; 522 | top: 0; 523 | left: 0; 524 | background-color: rgba(0, 0, 0, 0.45); 525 | } 526 | 527 | .modal{ 528 | z-index: 999; 529 | position: fixed; 530 | width: 70vw; 531 | height: 60vh; 532 | background-color: var(--cardBg); 533 | box-shadow: 0px 0px 25px rgba(0, 0, 0, 0.55); 534 | top: 50%; 535 | left: 50%; 536 | transform: translate(-50%, -50%); 537 | border-radius: 50px; 538 | overflow: hidden; 539 | 540 | .inner{ 541 | padding: 50px; 542 | overflow-y: auto; 543 | height: 75%; 544 | } 545 | 546 | .header{ 547 | display: grid; 548 | grid-template-columns: 1fr 15px; 549 | justify-content: space-between; 550 | 551 | svg{ 552 | font-size: 25px; 553 | padding-top: 10px; 554 | } 555 | } 556 | 557 | a, svg.close-icon{ 558 | color: var(--text); 559 | text-decoration: none; 560 | transition: 0.3s opacity ease-in-out; 561 | cursor: pointer; 562 | 563 | &:hover{ 564 | opacity: 0.85; 565 | } 566 | } 567 | 568 | @keyframes numb { 569 | 0% { 570 | transform: translateY(10px); 571 | } 572 | 573 | 50%{ 574 | transform: translateY(-10px); 575 | } 576 | 577 | 100%{ 578 | transform: translateY(10px); 579 | } 580 | } 581 | 582 | .deleting{ 583 | .no-anim{ 584 | animation: none; 585 | } 586 | 587 | svg{ 588 | text-align: center; 589 | font-size: 80px; 590 | margin: 0 auto; 591 | display: block; 592 | padding: 50px 0; 593 | animation: numb 1s ease-in-out 0s infinite; 594 | } 595 | 596 | p{ 597 | text-align: center; 598 | width: 30vw; 599 | margin: 0 auto 15px auto; 600 | } 601 | 602 | 603 | } 604 | 605 | .content{ 606 | ul{ 607 | list-style-type: none; 608 | padding: 0px; 609 | margin: 0px; 610 | display: grid; 611 | grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); 612 | grid-gap: 10px; 613 | padding-top: 15px; 614 | 615 | li{ 616 | background-color: var(--innerElem); 617 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.35); 618 | padding: 20px; 619 | border-radius: 10px; 620 | 621 | .header svg{ 622 | font-size: 18px; 623 | padding-top: 0px; 624 | padding-left: 5px; 625 | } 626 | 627 | a{ 628 | white-space: nowrap; 629 | overflow: hidden; 630 | text-overflow: ellipsis; 631 | } 632 | 633 | p{ 634 | margin: 0px; 635 | padding-top: 10px; 636 | font-size: 14px; 637 | opacity: 0.85; 638 | } 639 | } 640 | } 641 | 642 | .delete-button{ 643 | bottom: 0; 644 | position: absolute; 645 | width: 100%; 646 | left: 0; 647 | height: 50px; 648 | background-color: #7f0505; 649 | border: 0px; 650 | outline: 0px; 651 | color: #fff; 652 | text-transform: uppercase; 653 | font-size: 15px; 654 | box-shadow: var(--boxShadow); 655 | transition: 0.3s background-color ease-in-out; 656 | cursor: pointer; 657 | 658 | &:hover{ 659 | background-color: #710505; 660 | } 661 | 662 | svg{ 663 | padding-right: 15px; 664 | } 665 | } 666 | 667 | } 668 | } 669 | } 670 | 671 | .mm-open{ 672 | overflow: hidden; 673 | } 674 | 675 | footer.welc{ 676 | position: fixed; 677 | bottom: 0; 678 | left: 0; 679 | } 680 | 681 | @media(max-width: 1180px){ 682 | 683 | .welcome{ 684 | padding: 10px; 685 | width: 90vw; 686 | 687 | .logo-cont{ 688 | img{ 689 | width: 80px; 690 | } 691 | 692 | padding-bottom: 10px; 693 | } 694 | 695 | h1{ 696 | font-size: 28px; 697 | } 698 | 699 | h2{ 700 | font-size: 20px; 701 | } 702 | 703 | } 704 | 705 | .nav-items{ 706 | display: none !important; 707 | } 708 | 709 | .mobileMenu{ 710 | display: block !important; 711 | font-size: 30px; 712 | margin-top: 10px; 713 | } 714 | 715 | .search-box{ 716 | margin-top: 85px !important; 717 | } 718 | 719 | .backdrop{ 720 | margin-top: 120px; 721 | } 722 | 723 | .mobile-open{ 724 | height: 100%; 725 | position: fixed; 726 | width: 100%; 727 | z-index: 77; 728 | left: 0; 729 | top: 0; 730 | margin: 0px !important; 731 | display: block !important; 732 | position: fixed; 733 | background-color: var(--innerElem); 734 | padding-top: 35px; 735 | 736 | .mm{ 737 | display: block !important; 738 | } 739 | 740 | .brand{ 741 | padding-left: 25px; 742 | padding-bottom: 20px; 743 | } 744 | 745 | .closeMenu{ 746 | float: right; 747 | margin-right: 25px; 748 | font-size: 30px; 749 | margin-top: 10px; 750 | z-index: 9999999; 751 | } 752 | 753 | .mobileMenu{ 754 | display: none !important; 755 | } 756 | 757 | .nav-items{ 758 | display: block !important; 759 | z-index: 999999999; 760 | background-color: var(--innerElem); 761 | overflow-y: auto; 762 | height: calc(100vh - 125px); 763 | 764 | .nav-item{ 765 | margin: 15px 25px; 766 | } 767 | } 768 | } 769 | } 770 | 771 | .mobileMenu, .mm{ 772 | display: none; 773 | } 774 | 775 | @media(max-width: 730px){ 776 | body{ 777 | padding: 0 25px; 778 | } 779 | 780 | .dashboard{ 781 | header{ 782 | margin: 35px 0; 783 | 784 | img{ 785 | width: 40px !important; 786 | } 787 | } 788 | 789 | .repo-list{ 790 | display: block; 791 | 792 | h3{ 793 | margin-top: 5px; 794 | } 795 | 796 | ul{ 797 | padding-right: 5px !important; 798 | } 799 | 800 | .repo{ 801 | padding: 15px !important; 802 | } 803 | } 804 | 805 | .group{ 806 | border-radius: 20px !important; 807 | padding: 10px 15px !important; 808 | margin-bottom: 20px; 809 | } 810 | 811 | .modal{ 812 | width: 100%; 813 | top: 0; 814 | height: 100%; 815 | transform: none; 816 | left: 0; 817 | border-radius: 0px; 818 | } 819 | } 820 | 821 | .notification{ 822 | width: 100% !important; 823 | bottom: 0px !important; 824 | border-radius: 0px !important; 825 | transform: none !important; 826 | left: 0 !important; 827 | animation: showupm 0.3s 0.1s forwards cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; 828 | display: block !important; 829 | padding: 0px !important; 830 | 831 | p{ 832 | padding: 0 25px; 833 | } 834 | svg{ 835 | display: none; 836 | } 837 | 838 | } 839 | 840 | .remove{ 841 | animation: hideawaym 0.4s 0s forwards cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; 842 | } 843 | 844 | 845 | } 846 | 847 | footer.site-footer{ 848 | padding: 35px 0; 849 | text-align: center; 850 | width: 100%; 851 | opacity: 0; 852 | margin-top: 20px;; 853 | animation: tgb 0.5s 1.5s forwards cubic-bezier(0.68, -0.55, 0.265, 1.55); 854 | 855 | a{ 856 | color: var(--text); 857 | text-decoration: none; 858 | opacity: 0.75; 859 | transition: 0.3s opactiy ease-in-out; 860 | 861 | &:hover{ 862 | opacity: 1; 863 | } 864 | } 865 | 866 | 867 | } 868 | 869 | @keyframes hideawaym{ 870 | 0% { 871 | opacity: 1; 872 | transform: translateY(0px); 873 | } 874 | 875 | 100%{ 876 | opacity: 0; 877 | display: none; 878 | transform: translateY(50px); 879 | } 880 | } 881 | 882 | @keyframes showupm { 883 | 0% { 884 | opacity: 0; 885 | transform: translateY(50px); 886 | } 887 | 888 | 100%{ 889 | opacity: 1; 890 | transform: translateY(0px); 891 | } 892 | } 893 | 894 | @keyframes hideaway{ 895 | 0% { 896 | opacity: 1; 897 | transform: translate(-50%, 0px); 898 | } 899 | 900 | 100%{ 901 | opacity: 0; 902 | display: none; 903 | transform: translate(-50%, 50px); 904 | } 905 | } 906 | 907 | @keyframes showup { 908 | 0% { 909 | opacity: 0; 910 | transform: translate(-50%, 50px); 911 | } 912 | 913 | 100%{ 914 | opacity: 1; 915 | transform: translate(-50%, 0px); 916 | } 917 | } 918 | 919 | .notification{ 920 | position: fixed; 921 | color: #fff; 922 | background-color: green; 923 | box-shadow: var(--boxShadow); 924 | border-radius: 20px; 925 | padding: 10px 25px; 926 | display: grid; 927 | grid-template-columns: 20px 1fr; 928 | align-items: center; 929 | grid-gap: 20px; 930 | left: 50%; 931 | bottom: 20px; 932 | transform: translateX(-50%); 933 | z-index: 10000; 934 | width: 400px; 935 | opacity: 0; 936 | animation: showup 0.3s 0.1s forwards cubic-bezier(0.68, -0.55, 0.265, 1.55); 937 | 938 | svg{ 939 | margin-right: 15px; 940 | font-size: 20px; 941 | } 942 | } 943 | 944 | @media(min-width: 731px){ 945 | .remove{ 946 | animation: hideaway 0.4s 0s forwards cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; 947 | } 948 | } -------------------------------------------------------------------------------- /src/views/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Nav from "../components/Nav"; 3 | import { FiGlobe, FiLock, FiTrash2, FiXCircle, FiThumbsDown, FiThumbsUp, FiFrown, FiSmile, FiInfo} from "react-icons/fi"; 4 | import { PropagateLoader, BounceLoader } from "react-spinners"; 5 | import Repo from '../components/Repo'; 6 | 7 | class Dashboard extends Component { 8 | constructor(props){ 9 | super(props) 10 | 11 | this.state = { 12 | repos: {}, 13 | deleteWarn: false, 14 | deletedRepos: [], 15 | loading: true, 16 | deleting: false, 17 | searchOpen: false, 18 | notification: { 19 | showing: false, 20 | message: "" 21 | } 22 | } 23 | 24 | } 25 | 26 | sortByStar(obj){ 27 | let repos = Object.values(obj).sort((a,b)=>a.stargazers_count-b.stargazers_count).reverse(); 28 | let deletedRepos = this.state.deletedRepos.map(r => r.name); 29 | 30 | this.setState({ 31 | deletedRepos: this.state.deletedRepos.filter(r => repos.map(r => r.name).includes(r.name)) 32 | }) 33 | 34 | repos = repos.filter(r => !deletedRepos.includes(r.name)) 35 | 36 | return repos; 37 | } 38 | 39 | componentWillMount(){ 40 | this.loadRepos(); 41 | } 42 | 43 | loadRepos(){ 44 | fetch("/api/repos").then(res => res.json()).then((res) => { 45 | this.setState({repos: this.sortByStar(res), loading: false}) 46 | }).catch(err => console.log(err)) 47 | } 48 | 49 | addToDelete(repo){ 50 | let repos = Object.values(this.state.repos); 51 | repos.map(r => r.cleanupStatus = ""); 52 | 53 | this.setState({ 54 | repos: {...repos.filter(r => r.name !== repo.name)}, 55 | deletedRepos: [repo, ...this.state.deletedRepos] 56 | }) 57 | 58 | this.showNotification(`Your repository ${repo.name} has been selected for cleanup.`) 59 | } 60 | 61 | removeFromDelete(repo, addBack=true){ 62 | let deletedRepos = this.state.deletedRepos.filter(i => i.name !== repo.name); 63 | let repos = Object.values(this.state.repos) 64 | 65 | if(addBack){ 66 | repos.unshift(repo); 67 | } 68 | 69 | this.setState({ 70 | repos: {...repos}, 71 | deletedRepos 72 | }) 73 | 74 | if(addBack){ 75 | this.showNotification(`Your repository ${repo.name} is no longer selected for cleanup.`) 76 | } 77 | } 78 | 79 | deleteRepos(){ 80 | let {deletedRepos} = this.state; 81 | 82 | deletedRepos.map(r => r.cleanupStatus = "deleting"); 83 | 84 | this.setState({deletedRepos}) 85 | 86 | if(deletedRepos.length === 0){ 87 | return; 88 | } 89 | 90 | this.setState({deleting: true}) 91 | 92 | let app = this; 93 | let status = "success"; 94 | let deleteFromApi = deletedRepos.map((repo) => { 95 | return new Promise((resolve, reject) => { 96 | fetch(`/api/repos/delete/${encodeURIComponent(repo.full_name)}`).then(res => res.json()).then((res) => { 97 | repo.cleanupMessage = res.message; 98 | 99 | if(res.statusCode === 204){ 100 | repo.cleanupStatus = "success" 101 | deletedRepos = deletedRepos.filter(i => i.name !== repo.name); 102 | app.removeFromDelete(repo, false) 103 | } else{ 104 | repo.cleanupStatus = "error" 105 | status = "error"; 106 | } 107 | 108 | app.setState({deletedRepos}); 109 | 110 | resolve(status); 111 | }).catch(err => reject(err)); 112 | }) 113 | 114 | }) 115 | 116 | Promise.all(deleteFromApi).then((status) => { 117 | if(status.some(i => i === "error")){ 118 | this.setState({deleting: "error"}) 119 | } else{ 120 | this.setState({deleting: "success"}) 121 | 122 | } 123 | }) 124 | 125 | 126 | } 127 | 128 | closeModal(){ 129 | let {deletedRepos} = this.state; 130 | 131 | deletedRepos.map((repo) => { 132 | repo.cleanupMessage = ""; 133 | repo.cleanupStatus = ""; 134 | return repo; 135 | }) 136 | 137 | this.setState({ 138 | deleteWarn: false, 139 | deleting: false, 140 | currentDelete: "", 141 | deletedRepos 142 | }) 143 | 144 | document.body.classList.remove("mm-open") 145 | } 146 | 147 | showNotification(message){ 148 | this.setState({ 149 | notification: { 150 | showing: true, 151 | message 152 | } 153 | }) 154 | 155 | if(this.timer !== null){ 156 | clearTimeout(this.timer) 157 | } 158 | 159 | this.timer = setTimeout(() => { 160 | let d = document.getElementsByClassName("notification"); 161 | 162 | if(d.length === 1){ 163 | d[0].classList.add("remove") 164 | } 165 | 166 | setTimeout(() => { 167 | this.setState({ 168 | notification: { 169 | showing: false, 170 | message: "" 171 | } 172 | }) 173 | }, 400) 174 | 175 | 176 | this.timer = null; 177 | }, 2500) 178 | 179 | } 180 | 181 | componentWillUnmount(){ 182 | clearTimeout(this.timer) 183 | } 184 | 185 | render() { 186 | let {repos, deletedRepos, loading, deleteWarn, deleting, notification} = this.state; 187 | repos = Object.values(repos); 188 | let privateRepos = repos.filter(repo => repo.private); 189 | let publicRepos = repos.filter(repo => !repo.private); 190 | 191 | return ( 192 |
    e.keyCode === 27 ? this.closeModal() : null}> 193 |
    341 | ); 342 | } 343 | } 344 | 345 | export default Dashboard; -------------------------------------------------------------------------------- /src/views/Splash.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { GoMarkGithub } from "react-icons/go"; 3 | import logo from "../assets/logo.png"; 4 | import lightLogo from "../assets/lightLogo.png"; 5 | 6 | class Splash extends Component { 7 | 8 | render() { 9 | return ( 10 |
    11 |
    12 | GitCleanup logo 13 | GitCleanup logo 14 |
    15 |

    Welcome to GitCleanup

    16 |

    Say hello to GitHub Zero: de-clutter your GitHub profile and get rid of unused repositories with just a few clicks.

    17 | Login with GitHub 18 |
    19 | ); 20 | } 21 | } 22 | 23 | export default Splash; --------------------------------------------------------------------------------