├── .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 | 
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 |
15 | - - Fetches your skills
16 | - - Fetches your details
17 | - - Links to your repos
18 |
19 |
20 |
21 |
24 |
30 |
35 | Generate Resume
36 |
37 |
38 |
39 |
40 |
41 |
})
46 |
47 |
48 |
GitZilla
49 |
A resume builder for your GitHub profile
50 |
})
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 |
115 | )}
116 | {userData.company && (
117 |
118 |
119 | {userData.company}
120 |
121 | )}
122 | {userData.websiteUrl && (
123 |
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 |
--------------------------------------------------------------------------------