├── .env.sample ├── .github └── release-drafter.yml ├── .gitignore ├── .netlify └── state.json ├── LICENSE ├── README.md ├── gitzilla.jpg ├── package-lock.json ├── package.json ├── public ├── _redirects └── index.html └── src ├── assets └── femalecodertocat.png ├── components ├── App.jsx ├── Container.jsx ├── Home.jsx ├── RepoCard.jsx ├── User.jsx └── useRepoSearch.js ├── index.js ├── index.scss ├── serviceWorker.js ├── utils ├── graphqlClient.js └── queries.js └── variables.scss /.env.sample: -------------------------------------------------------------------------------- 1 | REACT_APP_GITHUB_TOKEN= -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$NEXT_PATCH_VERSION 🌈' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | categories: 4 | - title: '🚀 Features' 5 | labels: 6 | - 'feature' 7 | - 'enhancement' 8 | - 'feat' 9 | - 'FEAT' 10 | - 'FEATURE' 11 | - title: '🐛 Bug Fixes' 12 | labels: 13 | - 'fix' 14 | - 'bugfix' 15 | - 'bug' 16 | - title: '🧰 Maintenance' 17 | label: 'chore' 18 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 19 | template: | 20 | ## Changes 21 | $CHANGES 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.netlify/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteId": "16350c75-69db-4fee-b01b-d6be7d14cf5b" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gitzilla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # GitZilla :star2: 3 | 4 | ![GitZilla Website](./gitzilla.jpg) 5 | 6 | ## :woman_technologist: Technology Stack 7 | * [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) 8 | * [React](https://reactjs.org) 9 | * [React Router](https://reactrouter.com/) 10 | * [Netlify](https://www.netlify.com/) 11 | * [CSS Animations](https://www.youtube.com/playlist?list=PL4cUxeGkcC9iGYgmEd2dm3zAKzyCGDtM5) 12 | * [SASS](https://sass-lang.com/documentation/syntax) 13 | 14 | ## :round_pushpin: API 15 | [GitHub API](https://docs.github.com/en/graphql) 16 | 17 | ## :zap: Installation 18 | **1. Clone this repo by running either of the below commands.** 19 | 20 | https : `git clone https://github.com/ChoukseyKhushbu/Gitzilla.git` 21 | 22 | ssh : `git clone git@github.com:ChoukseyKhushbu/Gitzilla.git` 23 | 24 | **2. Now, run the following commands:** 25 | 26 | ```bash 27 | cd Gitzilla 28 | npm install 29 | ``` 30 | This will install all the project dependencies. 31 | 32 | **3. Create a `.env` file in the project root folder and copy the format of `.env.sample` file.** 33 | 34 | - `.env.sample` file contains all the environment variables required for running the project. 35 | 36 | **4. Get your `Personal Access Token` by signing in to your github account and then go to your setting -> developer setting -> Personal access tokens -> Generate new token** 37 | 38 | - Add this token to your `.env` file 39 | 40 | **5. To start the development server run:** 41 | ```bash 42 | npm start 43 | ``` 44 | **6. :tada: Open your browser and go to `https://localhost:3000`** 45 | 46 | ## :page_facing_up: License 47 | [MIT](./LICENSE) © Gitzilla 48 | -------------------------------------------------------------------------------- /gitzilla.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChoukseyKhushbu/Gitzilla/f5029cbfbd88beb7d17c842bfc1083055cf82603/gitzilla.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-resume", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.0", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "graphql-request": "^2.0.0", 11 | "node-sass": "^4.14.1", 12 | "react": "^16.13.1", 13 | "react-dom": "^16.13.1", 14 | "react-router-dom": "^5.2.0", 15 | "react-scripts": "3.4.1" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": "react-app" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | GitZilla 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/femalecodertocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChoukseyKhushbu/Gitzilla/f5029cbfbd88beb7d17c842bfc1083055cf82603/src/assets/femalecodertocat.png -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Route, Switch } from "react-router-dom"; 3 | import Home from "./Home"; 4 | import User from "./User"; 5 | import Container from "./Container"; 6 | function App() { 7 | const [userName, setUserName] = useState(""); 8 | 9 | const changeName = (name) => { 10 | console.log(name); 11 | setUserName(name); 12 | }; 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /src/components/Container.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | const Container = (props) => { 3 | return
{props.children}
; 4 | }; 5 | 6 | export default Container; 7 | -------------------------------------------------------------------------------- /src/components/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | const Home = (props) => { 4 | const handleChange = async (e) => { 5 | const newUser = e.target.value; 6 | props.changeName(newUser); 7 | }; 8 | return ( 9 | <> 10 |
11 |
12 |

GitZilla

13 |

A resume builder for your GitHub profile.

14 | 19 | 20 |
21 | 24 | 30 | 35 | Generate Resume 36 | 37 |
38 |
39 |
40 |
41 |  resume builder 46 |
47 |
48 |

GitZilla

49 |

A resume builder for your GitHub profile

50 |  resume builder 55 |
56 |
57 |
58 | 61 | 67 | 72 | Generate Resume 73 | 74 |
75 |
76 | 77 | ); 78 | }; 79 | export default Home; 80 | -------------------------------------------------------------------------------- /src/components/RepoCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const RepoCard = React.forwardRef((props, ref) => { 4 | let { repo } = props; 5 | return ( 6 | 13 |
{repo.name}
14 |

{repo.description}

15 |
16 | {repo.languages && 17 | repo.languages.length > 0 && 18 | repo.languages.map((s) => {s})} 19 |
20 |
21 | ); 22 | }); 23 | 24 | export default RepoCard; 25 | -------------------------------------------------------------------------------- /src/components/User.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import { useEffect, useState } from "react"; 4 | import RepoCard from "./RepoCard"; 5 | import { Link } from "react-router-dom"; 6 | import useRepoSearch from "./useRepoSearch"; 7 | import { useRef } from "react"; 8 | import { useCallback } from "react"; 9 | import { graphqlClient } from "../utils/graphqlClient"; 10 | import { searchUserQuery } from "../utils/queries"; 11 | 12 | const User = () => { 13 | let { userName } = useParams(); 14 | const [userData, setuserData] = useState(null); 15 | const [userFound, isUserFound] = useState(true); 16 | const [currCursor, setCurrCursor] = useState(null); 17 | 18 | const { 19 | reposLoading, 20 | error, 21 | repos, 22 | hasMore, 23 | skills, 24 | nextCursor, 25 | } = useRepoSearch(userName, currCursor); 26 | 27 | const observer = useRef(); 28 | const lastRepoElementRef = useCallback( 29 | (node) => { 30 | if (reposLoading) return; 31 | if (observer.current) observer.current.disconnect(); 32 | observer.current = new IntersectionObserver((entries) => { 33 | if (entries[0].isIntersecting && hasMore && currCursor !== nextCursor) { 34 | setCurrCursor(nextCursor); 35 | } 36 | }); 37 | if (node) observer.current.observe(node); 38 | }, 39 | [reposLoading, hasMore, nextCursor, currCursor] 40 | ); 41 | 42 | useEffect(() => { 43 | (async () => { 44 | try { 45 | const res = await graphqlClient.request(searchUserQuery, { 46 | username: userName, 47 | }); 48 | setuserData(res.user); 49 | } catch (error) { 50 | isUserFound(false); 51 | } 52 | })(); 53 | }, [userName]); 54 | 55 | return userFound ? ( 56 | <> 57 |
58 |
59 |
60 | {userData && ( 61 | 62 | )} 63 |
64 | {userData ? ( 65 | <> 66 |

67 | {userData.name && userData.name.length > 0 68 | ? userData.name 69 | : userName} 70 |

71 |

{userData.bio}

72 | 73 | ) : ( 74 | <> 75 | {/* eslint-disable-next-line jsx-a11y/heading-has-content */} 76 |

77 |

78 | 79 | )} 80 |
81 |
82 |

Skills

83 |
84 | {Object.keys(skills).length > 0 || repos.length > 0 85 | ? Object.keys(skills).map((s) => {s}) 86 | : [...Array(5)].map((_, index) => ( 87 | 92 | ))} 93 |
94 |
95 | {userData && ( 96 |
97 | {userData.followers.totalCount} followers 98 | {userData.following.totalCount} following 99 |
100 | )} 101 | {userData && ( 102 |
103 | {userData.url && ( 104 |
105 | 106 | 112 | {'@' + userName} 113 | 114 |
115 | )} 116 | {userData.company && ( 117 |
118 | 119 | {userData.company} 120 |
121 | )} 122 | {userData.websiteUrl && ( 123 |
124 | 125 | 131 | {userData.websiteUrl} 132 | 133 |
134 | )} 135 | {userData.location && ( 136 |
137 | 138 | {userData.location} 139 |
140 | )} 141 |
142 | )} 143 |
144 |
145 |

Repositories

146 |
147 | {repos.map((repo, index) => { 148 | if (repos.length === index + 1) { 149 | return ( 150 | 151 | ); 152 | } else return ; 153 | })} 154 | {reposLoading && 155 | !error && 156 | [...Array(12)].map((_, index) => ( 157 |
162 | ))} 163 |
164 |
165 | 166 | ) : ( 167 |
168 |

Ooops!!

169 |

404 User Not Found

170 | 171 | Go Back 172 | 173 |
174 | ); 175 | }; 176 | 177 | export default User; 178 | -------------------------------------------------------------------------------- /src/components/useRepoSearch.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { graphqlClient } from "../utils/graphqlClient"; 3 | import { searchRepoQuery } from "../utils/queries"; 4 | 5 | export default function useRepoSearch(username, currCursor) { 6 | const [reposLoading, setReposLoading] = useState(true); 7 | const [error, setError] = useState(false); 8 | const [repos, setRepos] = useState([]); 9 | const [hasMore, setHasMore] = useState(false); 10 | const [nextCursor, setNextCursor] = useState(null); 11 | const [skills, setSkills] = useState({}); 12 | 13 | useEffect(() => { 14 | setRepos([]); 15 | }, [username]); 16 | 17 | useEffect(() => { 18 | setReposLoading(true); 19 | setError(false); 20 | 21 | graphqlClient 22 | .rawRequest(searchRepoQuery, { username: username, after: currCursor }) 23 | .then((res) => { 24 | const { hasNextPage, endCursor } = res.data.user.repositories.pageInfo; 25 | const repos = toRepositories(res.data); 26 | setRepos((previousRepos) => { 27 | return [...previousRepos, ...repos]; 28 | }); 29 | let obj = skills; 30 | repos.forEach((repo) => { 31 | if (repo.languages) { 32 | repo.languages.forEach((language) => { 33 | obj[language] = obj[language] ? obj[language]++ : 1; 34 | }); 35 | } 36 | }); 37 | setSkills(obj); 38 | setHasMore(hasNextPage); 39 | setNextCursor(endCursor); 40 | setReposLoading(false); 41 | }) 42 | .catch((e) => { 43 | setError(true); 44 | }); 45 | }, [username, currCursor, skills]); 46 | return { reposLoading, error, repos, hasMore, skills, nextCursor }; 47 | } 48 | 49 | function toRepositories(data) { 50 | let result = []; 51 | if (data.user) { 52 | result = data.user.repositories.nodes.map((x) => { 53 | return { 54 | id: x.id, 55 | name: x.name, 56 | description: x.description, 57 | url: x.url, 58 | languages: x.languages.nodes.map((l) => { 59 | return l.name; 60 | }), 61 | }; 62 | }); 63 | } 64 | return result; 65 | } 66 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.scss"; 4 | import App from "./components/App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | import { BrowserRouter } from "react-router-dom"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://bit.ly/CRA-PWA 18 | serviceWorker.unregister(); 19 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.scss"; 2 | 3 | body { 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 6 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 7 | sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | color: $light; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 15 | monospace; 16 | } 17 | 18 | .mobileTop { 19 | display: none; 20 | } 21 | 22 | .mobileBottom { 23 | display: none; 24 | } 25 | 26 | .container { 27 | display: flex; 28 | flex-direction: row; 29 | min-height: 100vh; 30 | } 31 | 32 | .sidebar { 33 | box-sizing: border-box; 34 | padding: 20px; 35 | width: 30%; 36 | background-color: $darkBlue; 37 | height: 100vh; 38 | position: sticky; 39 | top: 0; 40 | bottom: 0; 41 | 42 | &.user { 43 | display: flex; 44 | flex-direction: column; 45 | overflow-y: scroll; 46 | } 47 | } 48 | 49 | .footer { 50 | position: absolute; 51 | bottom: 10px; 52 | } 53 | 54 | .main { 55 | background-color: $lightBlue; 56 | display: flex; 57 | flex-direction: column; 58 | 59 | &.home { 60 | align-items: center; 61 | justify-content: center; 62 | } 63 | flex: 1; 64 | padding: 20px; 65 | overflow: hidden; 66 | } 67 | 68 | .octocat { 69 | position: absolute; 70 | width: 150%; 71 | left: -219px; 72 | top: 50%; 73 | z-index: 1; 74 | transform: translateY(-50%); 75 | max-width: 680px; 76 | } 77 | 78 | li { 79 | list-style-type: none; 80 | padding-bottom: 5px; 81 | } 82 | 83 | .heading { 84 | font-size: 5rem; 85 | margin-block-start: 0; 86 | } 87 | 88 | .inputContainer { 89 | margin-top: 60px; 90 | } 91 | 92 | a { 93 | text-decoration: none; 94 | } 95 | 96 | .button { 97 | display: inline-block; 98 | background-color: $pink; 99 | color: $white; 100 | padding: 10px 15px; 101 | border-radius: 5px; 102 | transition: all 0.3s ease-in-out; 103 | 104 | &:hover { 105 | background-color: $lightPink; 106 | } 107 | } 108 | 109 | input[type="text"] { 110 | width: 70%; 111 | display: block; 112 | padding: 10px 15px; 113 | margin: 10px 0 20px 0; 114 | border-radius: 5px; 115 | background: #f0f1f9; 116 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 117 | } 118 | 119 | .avatar { 120 | background-color: gray; 121 | width: 100px; 122 | height: 100px; 123 | display: block; 124 | border-radius: 50px; 125 | 126 | img { 127 | height: 100%; 128 | width: 100%; 129 | border-radius: 50px; 130 | } 131 | } 132 | 133 | div.user > div:first-child { 134 | margin: 10px 0; 135 | } 136 | 137 | .skills { 138 | display: flex; 139 | flex-direction: row; 140 | flex-wrap: wrap; 141 | } 142 | 143 | .skills span { 144 | background-color: $light; 145 | text-align: center; 146 | color: black; 147 | padding: 8px 10px; 148 | border-radius: 2px; 149 | margin: 0 15px 15px 0; 150 | } 151 | 152 | .repoContainer { 153 | display: grid; 154 | grid-template-columns: repeat(3, calc((100% - 40px) / 3)); 155 | gap: 20px; 156 | position: relative; 157 | } 158 | 159 | .repoCard { 160 | box-sizing: border-box; 161 | display: flex; 162 | flex-direction: column; 163 | border-radius: 4px; 164 | padding: 20px 20px 12px 20px; 165 | background-color: $darkBlueOpacity05; 166 | position: relative; 167 | transition: 0.2s all ease-in-out; 168 | 169 | span { 170 | font-size: 0.75rem; 171 | background-color: $light; 172 | text-align: center; 173 | color: black; 174 | border-radius: 2px; 175 | padding: 4px 6px; 176 | // position: absolute; 177 | // bottom: 15px; 178 | overflow-wrap: break-word; 179 | margin: 0 8px 8px 0; 180 | display: inline-flex; 181 | } 182 | 183 | p { 184 | padding-bottom: 15px; 185 | color: $light50; 186 | overflow-wrap: break-word; 187 | flex: 1; 188 | } 189 | 190 | .repoTitle { 191 | font-size: 1.25rem; 192 | font-weight: bold; 193 | color: $light; 194 | overflow-wrap: break-word; 195 | } 196 | 197 | &:hover { 198 | background-color: $darkBlue; 199 | transform: scale(1.05); 200 | } 201 | } 202 | 203 | .social { 204 | display: flex; 205 | } 206 | 207 | .social > span { 208 | margin: 20px 20px 20px 0; 209 | } 210 | 211 | .details > div { 212 | i { 213 | padding-right: 5px; 214 | } 215 | 216 | margin: 10px 0; 217 | } 218 | 219 | ::-webkit-scrollbar { 220 | width: 8px; 221 | } 222 | 223 | ::-webkit-scrollbar-track { 224 | background: $darkBlue; 225 | } 226 | 227 | ::-webkit-scrollbar-thumb { 228 | background-color: $light20; 229 | border-radius: 6px; 230 | } 231 | 232 | @media only screen and (max-width: 1190px) { 233 | .repoContainer { 234 | grid-template-columns: repeat(2, calc((100% - 20px) / 2)); 235 | } 236 | } 237 | 238 | @media only screen and (max-width: 648px) { 239 | .container { 240 | flex-direction: column; 241 | } 242 | 243 | .sidebar.user { 244 | width: 100vw; 245 | overflow-y: hidden; 246 | position: unset; 247 | height: auto; 248 | } 249 | 250 | .repoContainer { 251 | display: grid; 252 | grid-template-columns: 100%; 253 | } 254 | } 255 | 256 | .leftContainer { 257 | background-color: $darkBlue; 258 | display: flex; 259 | flex-direction: column; 260 | align-items: center; 261 | justify-content: center; 262 | flex: 2; 263 | } 264 | 265 | .rightContainer { 266 | background-color: $lightBlue; 267 | flex: 1; 268 | position: relative; 269 | } 270 | 271 | .rightContainer::before { 272 | content: ""; 273 | width: 100%; 274 | position: absolute; 275 | top: 0; 276 | right: -47px; 277 | transform: skewX(-13deg) translateX(-50%); 278 | height: 100vh; 279 | background-color: $lightBlue; 280 | } 281 | 282 | ul { 283 | padding-inline-start: 0; 284 | } 285 | 286 | .content { 287 | position: absolute; 288 | font-size: 1.25rem; 289 | left: 17%; 290 | z-index: 2; 291 | } 292 | 293 | @media only screen and (max-width: 768px) { 294 | .leftContainer { 295 | display: none; 296 | } 297 | 298 | .rightContainer { 299 | display: none; 300 | } 301 | 302 | .sidebar.user { 303 | width: 100vw; 304 | overflow-y: hidden; 305 | position: unset; 306 | height: auto; 307 | } 308 | 309 | .container { 310 | flex-direction: column; 311 | overflow: hidden; 312 | background-color: $darkBlue; 313 | } 314 | 315 | .mobileTop { 316 | display: flex; 317 | flex-direction: column; 318 | background-color: $darkBlue; 319 | flex: 1; 320 | padding: 20px 41% 0 10%; 321 | 322 | p { 323 | font-size: 1.25rem; 324 | } 325 | } 326 | 327 | .mobileBottom { 328 | display: flex; 329 | background-color: transparent; 330 | flex: 1; 331 | position: relative; 332 | } 333 | 334 | .mobileBottom::before { 335 | content: ""; 336 | width: 100%; 337 | position: absolute; 338 | transform: skewY(-14deg) translateY(4%); 339 | height: 100vh; 340 | background-color: $lightBlue; 341 | } 342 | 343 | .inputContainer { 344 | z-index: 1; 345 | width: 100%; 346 | max-width: 400px; 347 | padding-left: 10%; 348 | padding-right: 20%; 349 | padding-top: 10%; 350 | } 351 | 352 | input[type="text"] { 353 | width: 100%; 354 | } 355 | 356 | .button { 357 | width: 100%; 358 | text-align: center; 359 | padding: 10px 17px; 360 | } 361 | 362 | .mobileOctocat { 363 | position: fixed; 364 | width: 68%; 365 | max-width: 300px; 366 | top: 117px; 367 | right: -25px; 368 | left: unset; 369 | z-index: 1; 370 | } 371 | } 372 | 373 | .error { 374 | text-align: center; 375 | height: 100vh; 376 | width: 100vw; 377 | background-color: $darkBlue; 378 | color: $light; 379 | 380 | h1 { 381 | font-size: 3rem; 382 | } 383 | } 384 | 385 | @media screen and (min-width: 770px) and (max-width: 966px) { 386 | .content { 387 | p { 388 | max-width: 300px; 389 | } 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/utils/graphqlClient.js: -------------------------------------------------------------------------------- 1 | import { GraphQLClient } from 'graphql-request' 2 | 3 | export const graphqlClient = new GraphQLClient('https://api.github.com/graphql', { 4 | headers: { 5 | Authorization: `Bearer ${process.env.REACT_APP_GITHUB_TOKEN}`, 6 | }, 7 | method: 'POST', 8 | }); -------------------------------------------------------------------------------- /src/utils/queries.js: -------------------------------------------------------------------------------- 1 | export const searchUserQuery = ` 2 | query searchUser($username: String!) { 3 | user(login: $username) { 4 | avatarUrl 5 | name 6 | url 7 | login 8 | bio 9 | websiteUrl 10 | location 11 | company 12 | following { 13 | totalCount 14 | } 15 | followers { 16 | totalCount 17 | } 18 | } 19 | } 20 | `; 21 | 22 | export const searchRepoQuery = ` 23 | query searchRepos($username: String!,$after:String) { 24 | user(login: $username) { 25 | repositories(orderBy: {field: UPDATED_AT, direction: DESC}, ownerAffiliations: OWNER, isFork: false, first: 30, after: $after) { 26 | nodes { 27 | id 28 | name 29 | description 30 | url 31 | languages(first: 5) { 32 | nodes { 33 | name 34 | } 35 | } 36 | } 37 | pageInfo { 38 | hasNextPage 39 | endCursor 40 | } 41 | totalCount 42 | } 43 | } 44 | } 45 | `; 46 | -------------------------------------------------------------------------------- /src/variables.scss: -------------------------------------------------------------------------------- 1 | $light: #c6c9cf; 2 | $light50: rgba(198, 201, 207, 0.7); 3 | $light20: rgba(198, 201, 207, 0.2); 4 | // $light50: #a0a0a0; 5 | $lightBlue: #202737; 6 | $lightBlue50: rgba(32, 39, 55, 0.1); 7 | $darkBlue: #171926; 8 | $darkBlueOpacity05: rgba(22, 24, 38, 0.4); 9 | 10 | $white: rgb(240, 240, 240); 11 | $pink: #d61252; 12 | $pink80: rgba(214, 18, 82, 0.8); 13 | $lightPink: rgba(214, 18, 82, 0.5); 14 | 15 | $sidebar-width: 250px; 16 | 17 | .shine { 18 | background: $lightBlue; 19 | background-image: linear-gradient( 20 | to right, 21 | $lightBlue 0%, 22 | $light20 20%, 23 | $lightBlue 40%, 24 | $lightBlue 100% 25 | ); 26 | background-repeat: no-repeat; 27 | background-size: 800px 100%; 28 | display: inline-block; 29 | position: relative; 30 | 31 | animation-duration: 1s; 32 | animation-fill-mode: forwards; 33 | animation-iteration-count: infinite; 34 | animation-name: placeholderShimmer; 35 | animation-timing-function: linear; 36 | } 37 | 38 | @keyframes placeholderShimmer { 39 | 0% { 40 | background-position: -468px 0; 41 | } 42 | 43 | 100% { 44 | background-position: 468px 0; 45 | } 46 | } 47 | 48 | .box { 49 | height: 128px; 50 | width: 128px; 51 | } 52 | 53 | .lines { 54 | border-radius: 8px; 55 | height: 1.2rem; 56 | width: 200px; 57 | } 58 | --------------------------------------------------------------------------------