├── .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 | [](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 | [](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 |

88 |

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 |
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 |

13 |

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;
--------------------------------------------------------------------------------