├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── @types
│ └── index.ts
├── App.tsx
├── components
│ ├── Footer
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── Header
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── ProfileData
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── RandomCalendar
│ │ ├── index.tsx
│ │ └── styles.ts
│ └── RepoCard
│ │ ├── index.tsx
│ │ └── styles.ts
├── index.tsx
├── pages
│ ├── Profile
│ │ ├── index.tsx
│ │ └── styles.ts
│ └── Repo
│ │ ├── index.tsx
│ │ └── styles.ts
├── react-app-env.d.ts
└── styles
│ ├── GlobalStyles.ts
│ └── themes.ts
├── tsconfig.json
└── yarn.lock
/.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.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | UI Clone - Github
3 |
4 |
5 | Responsive Github UI Clone (partial) for study purposes.
6 | Deployed here.
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ## Participants
20 |
21 | [
](https://github.com/guilhermerodz)
22 |
23 | [Guilherme Rodz](https://github.com/guilhermerodz)
24 |
25 | ## Techs
26 |
27 | - [x] Fetch API
28 | - [x] React.js
29 | - [x] Styled Components
30 | - [x] TypeScript
31 |
32 | ## Ideas to implement
33 |
34 | - [ ] Use [SWR](https://swr.vercel.app/) as cache invalidation strategy
35 | - [ ] Fetch data from [GitHub V4 API](https://docs.github.com/en/graphql) (GraphQL instead of REST).
36 | - [ ] Create new routes with [React Router](https://reactrouter.com/web/guides/quick-start) (e.g. Github Feed, complete Repo page)
37 |
38 | ## Starting Dev Environment
39 |
40 | 1. Run `npm install` or `yarn install`.
41 | 2. Run `yarn start` and access `http://localhost:3000`.
42 |
43 | ## Contributing
44 |
45 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests.
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "youtube-clone-github",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "@types/jest": "^24.0.0",
10 | "@types/node": "^12.0.0",
11 | "@types/react": "^16.9.0",
12 | "@types/react-dom": "^16.9.0",
13 | "date-fns": "^2.15.0",
14 | "history": "^5.0.0",
15 | "react": "^16.13.1",
16 | "react-calendar-heatmap": "^1.8.1",
17 | "react-dom": "^16.13.1",
18 | "react-icons": "^3.10.0",
19 | "react-router-dom": "^6.0.0-beta.0",
20 | "react-scripts": "3.4.1",
21 | "styled-components": "^5.1.1",
22 | "typescript": "~3.7.2"
23 | },
24 | "scripts": {
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test",
28 | "eject": "react-scripts eject"
29 | },
30 | "eslintConfig": {
31 | "extends": "react-app"
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | },
45 | "devDependencies": {
46 | "@types/react-calendar-heatmap": "^1.6.2",
47 | "@types/react-icons": "^3.0.0",
48 | "@types/react-router-dom": "^5.1.5",
49 | "@types/styled-components": "^5.1.2"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-content/youtube-clone-github/ac60073d67ee71cd90f33cf6a6a83d4b62603912/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-content/youtube-clone-github/ac60073d67ee71cd90f33cf6a6a83d4b62603912/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocketseat-content/youtube-clone-github/ac60073d67ee71cd90f33cf6a6a83d4b62603912/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/@types/index.ts:
--------------------------------------------------------------------------------
1 | export interface APIUser {
2 | login: string;
3 | name: string;
4 | followers: number;
5 | following: number;
6 | public_repos: number;
7 | avatar_url: string;
8 | blog?: string;
9 | company?: string;
10 | email?: string;
11 | location?: string;
12 | }
13 |
14 | export interface APIRepo {
15 | name: string;
16 | owner: {
17 | login: string;
18 | };
19 | stargazers_count: number;
20 | forks: number;
21 | html_url: string;
22 | language?: string;
23 | description?: string;
24 | }
25 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import 'react-calendar-heatmap/dist/styles.css';
2 |
3 | import React, { useState } from 'react';
4 | import { BrowserRouter, Routes, Route } from 'react-router-dom';
5 | import { ThemeProvider } from 'styled-components';
6 |
7 | import GlobalStyles from './styles/GlobalStyles';
8 | import Header from './components/Header';
9 | import Profile from './pages/Profile';
10 | import Repo from './pages/Repo';
11 | import Footer from './components/Footer';
12 |
13 | import { ThemeName, themes } from './styles/themes';
14 |
15 | function App() {
16 | const [themeName, setThemeName] = useState('light');
17 | const currentTheme = themes[themeName];
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 | } />
26 | } />
27 | } />
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | export default App;
39 |
--------------------------------------------------------------------------------
/src/components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Container, Line, GithubLogo } from './styles';
4 |
5 | const Footer: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default Footer;
15 |
--------------------------------------------------------------------------------
/src/components/Footer/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { FaGithub } from 'react-icons/fa';
3 |
4 | export const Container = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 |
9 | margin: 25px 32px;
10 | `;
11 |
12 | export const Line = styled.div`
13 | max-width: 1280px;
14 | width: 100%;
15 | border-top: 1px solid var(--border);
16 | `;
17 |
18 | export const GithubLogo = styled(FaGithub)`
19 | margin-top: 25px;
20 |
21 | fill: var(--border);
22 | width: 24px;
23 | height: 24px;
24 | flex-shrink: 0;
25 | `;
26 |
--------------------------------------------------------------------------------
/src/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 |
4 | import { Container, GithubLogo, SearchForm } from './styles';
5 |
6 | import { ThemeName } from '../../styles/themes';
7 |
8 | interface Props {
9 | themeName: ThemeName;
10 | setThemeName: (newName: ThemeName) => void;
11 | }
12 |
13 | const Header: React.FC = ({ themeName, setThemeName }) => {
14 | const [search, setSearch] = useState('');
15 | const navigate = useNavigate();
16 |
17 | function handleSubmit(event: React.FormEvent) {
18 | event.preventDefault();
19 |
20 | navigate('/' + search.toLowerCase().trim());
21 | }
22 |
23 | function toggleTheme() {
24 | setThemeName(themeName === 'light' ? 'dark' : 'light');
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 | setSearch(e.currentTarget.value)}
35 | />
36 |
37 |
38 | );
39 | };
40 |
41 | export default Header;
42 |
--------------------------------------------------------------------------------
/src/components/Header/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { FaGithub } from 'react-icons/fa';
3 |
4 | export const Container = styled.div`
5 | display: flex;
6 | align-items: center;
7 | background: var(--header);
8 | padding: 11px 16px;
9 | `;
10 |
11 | export const GithubLogo = styled(FaGithub)`
12 | fill: var(--logo);
13 | width: 32px;
14 | height: 32px;
15 | flex-shrink: 0;
16 |
17 | cursor: pointer;
18 | &:hover {
19 | opacity: 0.8;
20 | }
21 | `;
22 |
23 | export const SearchForm = styled.form`
24 | padding-left: 16px;
25 | width: 100%;
26 |
27 | input {
28 | background: var(--search);
29 | outline: 0;
30 | border-radius: 6px;
31 | padding: 7px 12px;
32 | width: 100%;
33 |
34 | &:focus {
35 | width: 318px;
36 | }
37 |
38 | transition: width 0.2s ease-out, color 0.2s ease-out;
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/src/components/ProfileData/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | Container,
5 | Flex,
6 | Avatar,
7 | Row,
8 | PeopleIcon,
9 | Column,
10 | CompanyIcon,
11 | LocationIcon,
12 | EmailIcon,
13 | BlogIcon,
14 | } from './styles';
15 |
16 | interface Props {
17 | username: string;
18 | name: string;
19 | avatarUrl: string;
20 | followers: number;
21 | following: number;
22 | company?: string;
23 | location?: string;
24 | email?: string;
25 | blog?: string;
26 | }
27 |
28 | const ProfileData: React.FC = ({
29 | username,
30 | name,
31 | avatarUrl,
32 | followers,
33 | following,
34 | company,
35 | location,
36 | email,
37 | blog,
38 | }) => {
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
{name}
46 | {username}
47 |
48 |
49 |
50 |
51 |
52 |
53 | {followers}
54 | followers
55 | ·
56 |
57 |
58 | {following}
59 | following
60 |
61 |
62 |
63 |
64 | {company && (
65 |
66 |
67 | {company}
68 |
69 | )}
70 | {location && (
71 |
72 |
73 | {location}
74 |
75 | )}
76 | {email && (
77 |
78 |
79 | {email}
80 |
81 | )}
82 | {blog && (
83 |
84 |
85 | {blog}
86 |
87 | )}
88 |
89 |
90 | );
91 | };
92 |
93 | export default ProfileData;
94 |
--------------------------------------------------------------------------------
/src/components/ProfileData/styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 | import {
3 | RiGroupLine,
4 | RiBuilding4Line,
5 | RiMapPin2Line,
6 | RiMailLine,
7 | RiLinksLine,
8 | } from 'react-icons/ri';
9 |
10 | export const Container = styled.div``;
11 |
12 | export const Flex = styled.div`
13 | display: flex;
14 | align-items: center;
15 |
16 | > div {
17 | margin-left: 24px;
18 |
19 | > h1 {
20 | font-size: 26px;
21 | line-height: 1.25;
22 | color: var(--gray-dark);
23 | font-weight: 600;
24 | }
25 | > h2 {
26 | font-size: 20px;
27 | color: var(--username);
28 | font-weight: 300;
29 | }
30 | }
31 |
32 | @media (min-width: 768px) {
33 | flex-direction: column;
34 | align-items: flex-start;
35 |
36 | > div {
37 | margin-left: 0;
38 | margin-top: 16px;
39 | }
40 | }
41 | `;
42 |
43 | export const Avatar = styled.img`
44 | width: 16%;
45 | border-radius: 50%;
46 |
47 | @media (min-width: 768px) {
48 | width: 100%;
49 | margin-top: -34px;
50 | }
51 | `;
52 |
53 | export const Row = styled.ul`
54 | display: flex;
55 | align-items: center;
56 | flex-wrap: wrap;
57 | margin: 20px 0;
58 |
59 | > li {
60 | display: flex;
61 | align-items: center;
62 |
63 | > span {
64 | font-size: 14px;
65 | color: var(--gray);
66 | }
67 | > * {
68 | margin-right: 5px;
69 | }
70 | }
71 | `;
72 |
73 | const iconCSS = css`
74 | width: 16px;
75 | height: 16px;
76 | fill: var(--icon);
77 | flex-shrink: 0;
78 | `;
79 |
80 | export const PeopleIcon = styled(RiGroupLine)`
81 | ${iconCSS}
82 | `;
83 |
84 | export const Column = styled.ul`
85 | li {
86 | display: flex;
87 | align-items: center;
88 | font-size: 14px;
89 | }
90 | li + li {
91 | margin-top: 10px;
92 | }
93 | span {
94 | margin-left: 5px;
95 |
96 | overflow: hidden;
97 | text-overflow: ellipsis;
98 | white-space: nowrap;
99 | }
100 | `;
101 |
102 | export const CompanyIcon = styled(RiBuilding4Line)`
103 | ${iconCSS}
104 | `;
105 |
106 | export const LocationIcon = styled(RiMapPin2Line)`
107 | ${iconCSS}
108 | `;
109 |
110 | export const EmailIcon = styled(RiMailLine)`
111 | ${iconCSS}
112 | `;
113 |
114 | export const BlogIcon = styled(RiLinksLine)`
115 | ${iconCSS}
116 | `;
117 |
--------------------------------------------------------------------------------
/src/components/RandomCalendar/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Heatmap from 'react-calendar-heatmap';
3 | import { subYears, isBefore, isSameDay, addDays } from 'date-fns';
4 |
5 | import { Container } from './styles';
6 |
7 | type HeatmapValue = { date: Date; count: number };
8 |
9 | const RandomCalendar: React.FC = () => {
10 | const startDate = subYears(new Date(), 1);
11 | const endDate = new Date();
12 |
13 | return (
14 |
15 |
16 | {
22 | let clampedCount = 0;
23 |
24 | if (item !== null) {
25 | clampedCount = Math.max(item.count, 0);
26 | clampedCount = Math.min(item.count, 4);
27 | }
28 |
29 | return `scale-${clampedCount}`;
30 | }}
31 | showWeekdayLabels
32 | />
33 |
34 |
35 | Random calendar (do not represent actual data)
36 |
37 | );
38 | };
39 |
40 | const generateHeatmapValues = (startDate: Date, endDate: Date) => {
41 | const values: HeatmapValue[] = [];
42 |
43 | let currentDate = startDate;
44 | while (isBefore(currentDate, endDate) || isSameDay(currentDate, endDate)) {
45 | const count = Math.random() * 4;
46 |
47 | values.push({ date: currentDate, count: Math.round(count) });
48 |
49 | currentDate = addDays(currentDate, 1);
50 | }
51 |
52 | return values;
53 | };
54 |
55 | export default RandomCalendar;
56 |
--------------------------------------------------------------------------------
/src/components/RandomCalendar/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | align-items: flex-end;
7 | overflow: hidden;
8 |
9 | padding: 16px 20px 0 10px;
10 | border: 1px solid var(--border);
11 | border-radius: 6px;
12 |
13 | .wrapper {
14 | .scale-0 {
15 | fill: var(--calendar-scale-0);
16 | }
17 | .scale-1 {
18 | fill: var(--calendar-scale-1);
19 | }
20 | .scale-2 {
21 | fill: var(--calendar-scale-2);
22 | }
23 | .scale-3 {
24 | fill: var(--calendar-scale-3);
25 | }
26 | .scale-4 {
27 | fill: var(--calendar-scale-4);
28 | }
29 |
30 | width: 893px;
31 | }
32 |
33 | span {
34 | font-size: 11px;
35 | color: var(--link);
36 | margin-top: -25px;
37 | margin-left: 7px;
38 | padding-bottom: 16px;
39 |
40 | align-self: flex-start;
41 | }
42 | `;
43 |
--------------------------------------------------------------------------------
/src/components/RepoCard/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import {
5 | Container,
6 | Topside,
7 | RepoIcon,
8 | Botside,
9 | StarIcon,
10 | ForkIcon,
11 | } from './styles';
12 |
13 | interface Props {
14 | username: string;
15 | reponame: string;
16 | description?: string;
17 | language?: string;
18 | stars: number;
19 | forks: number;
20 | }
21 |
22 | const RepoCard: React.FC = ({
23 | username,
24 | reponame,
25 | description,
26 | language,
27 | stars,
28 | forks,
29 | }) => {
30 | const languageClass = language ? language.toLowerCase() : 'other';
31 |
32 | return (
33 |
34 |
35 |
36 |
37 | {reponame}
38 |
39 |
40 | {description}
41 |
42 |
43 |
44 |
45 | -
46 |
47 | {language}
48 |
49 | -
50 |
51 | {stars}
52 |
53 | -
54 |
55 | {forks}
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default RepoCard;
64 |
--------------------------------------------------------------------------------
/src/components/RepoCard/styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 | import { RiBookMarkLine, RiStarLine } from 'react-icons/ri';
3 | import { AiOutlineFork } from 'react-icons/ai';
4 |
5 | export const Container = styled.div`
6 | display: flex;
7 | flex-direction: column;
8 | justify-content: space-between;
9 | padding: 16px;
10 | border: 1px solid var(--border);
11 | border-radius: 6px;
12 | `;
13 |
14 | export const Topside = styled.div`
15 | > header {
16 | display: flex;
17 | align-items: center;
18 |
19 | > a {
20 | margin-left: 8px;
21 | font-size: 14px;
22 | font-weight: 600;
23 | color: var(--link);
24 |
25 | text-decoration: none;
26 |
27 | &:focus,
28 | &:hover {
29 | text-decoration: underline;
30 | }
31 | }
32 | }
33 |
34 | > p {
35 | margin: 8px 0 16px;
36 | font-size: 12px;
37 | color: var(--gray);
38 | letter-spacing: 0.1px;
39 | }
40 | `;
41 |
42 | const iconCSS = css`
43 | width: 16px;
44 | height: 16px;
45 | fill: var(--icon);
46 | flex-shrink: 0;
47 | `;
48 |
49 | export const RepoIcon = styled(RiBookMarkLine)`
50 | ${iconCSS}
51 | `;
52 |
53 | export const Botside = styled.div`
54 | > ul {
55 | display: flex;
56 | align-items: center;
57 |
58 | > li {
59 | display: flex;
60 | align-items: center;
61 | margin-right: 16px;
62 |
63 | > span {
64 | margin-left: 5px;
65 | font-size: 12px;
66 | color: var(--gray);
67 | }
68 | }
69 | }
70 |
71 | .language {
72 | width: 12px;
73 | height: 12px;
74 | border-radius: 50%;
75 | flex-shrink: 0;
76 |
77 | &.other {
78 | background: var(--other-language);
79 | }
80 | &.javascript {
81 | background: var(--javascript);
82 | }
83 | &.typescript {
84 | background: var(--typescript);
85 | }
86 | }
87 | `;
88 |
89 | export const StarIcon = styled(RiStarLine)`
90 | ${iconCSS}
91 | `;
92 |
93 | export const ForkIcon = styled(AiOutlineFork)`
94 | ${iconCSS}
95 | `;
96 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import App from './App';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/src/pages/Profile/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useParams } from 'react-router-dom';
3 |
4 | import {
5 | Container,
6 | Main,
7 | LeftSide,
8 | RightSide,
9 | Repos,
10 | CalendarHeading,
11 | RepoIcon,
12 | Tab,
13 | } from './styles';
14 |
15 | import ProfileData from '../../components/ProfileData';
16 | import RepoCard from '../../components/RepoCard';
17 | import RandomCalendar from '../../components/RandomCalendar';
18 |
19 | import { APIUser, APIRepo } from '../../@types';
20 |
21 | interface Data {
22 | user?: APIUser;
23 | repos?: APIRepo[];
24 | error?: string;
25 | }
26 |
27 | const Profile: React.FC = () => {
28 | const { username = 'guilhermerodz' } = useParams();
29 | const [data, setData] = useState();
30 |
31 | useEffect(() => {
32 | Promise.all([
33 | fetch(`https://api.github.com/users/${username}`),
34 | fetch(`https://api.github.com/users/${username}/repos`),
35 | ]).then(async (responses) => {
36 | const [userResponse, reposResponse] = responses;
37 |
38 | if (userResponse.status === 404) {
39 | setData({ error: 'User not found!' });
40 | return;
41 | }
42 |
43 | const user = await userResponse.json();
44 | const repos = await reposResponse.json();
45 |
46 | const shuffledRepos = repos.sort(() => 0.5 - Math.random());
47 | const slicedRepos = shuffledRepos.slice(0, 6); // 6 repos
48 |
49 | setData({
50 | user,
51 | repos: slicedRepos,
52 | });
53 | });
54 | }, [username]);
55 |
56 | if (data?.error) {
57 | return {data.error}
;
58 | }
59 |
60 | if (!data?.user || !data?.repos) {
61 | return Loading...
;
62 | }
63 |
64 | const TabContent = () => (
65 |
66 |
67 | Repositories
68 | {data.user?.public_repos}
69 |
70 | );
71 |
72 | return (
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | Random repos
106 |
107 |
108 | {data.repos.map((item) => (
109 |
118 | ))}
119 |
120 |
121 |
122 |
123 | Random calendar (do not represent actual data)
124 |
125 |
126 |
127 |
128 |
129 |
130 | );
131 | };
132 |
133 | export default Profile;
134 |
--------------------------------------------------------------------------------
/src/pages/Profile/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { RiBookMarkLine } from 'react-icons/ri';
3 |
4 | export const Container = styled.div`
5 | --horizontalPadding: 16px;
6 | --verticalPadding: 24px;
7 |
8 | padding: var(--verticalPadding) var(--horizontalPadding);
9 | overflow: hidden;
10 | `;
11 |
12 | export const Main = styled.div`
13 | display: flex;
14 | flex-direction: column;
15 |
16 | margin: 0 auto;
17 | max-width: 1280px;
18 |
19 | @media (min-width: 768px) {
20 | flex-direction: row;
21 | }
22 | `;
23 |
24 | export const LeftSide = styled.div`
25 | padding: 0 var(--horizontalPadding);
26 |
27 | @media (min-width: 768px) {
28 | width: 25%;
29 | }
30 | `;
31 |
32 | export const RightSide = styled.div`
33 | padding: 0 var(--horizontalPadding);
34 |
35 | @media (min-width: 768px) {
36 | width: 75%;
37 | }
38 | `;
39 |
40 | export const Repos = styled.div`
41 | margin-top: var(--verticalPadding);
42 |
43 | > h2 {
44 | font-size: 16px;
45 | font-weight: normal;
46 | }
47 | > div {
48 | margin-top: 8px;
49 |
50 | display: grid;
51 | grid-gap: 16px;
52 |
53 | grid-template-columns: 1fr;
54 |
55 | @media (min-width: 768px) {
56 | grid-template-columns: 1fr 1fr;
57 | grid-auto-rows: minmax(min-content, max-content);
58 | }
59 | }
60 | `;
61 |
62 | export const CalendarHeading = styled.span`
63 | font-size: 16px;
64 | margin: 36px 0 9px;
65 | display: inline-flex;
66 | `;
67 |
68 | export const RepoIcon = styled(RiBookMarkLine)`
69 | width: 16px;
70 | height: 16px;
71 | margin-right: 4px;
72 | `;
73 |
74 | export const Tab = styled.div`
75 | background: var(--primary);
76 |
77 | .content {
78 | display: flex;
79 | align-items: center;
80 | width: min-content;
81 |
82 | padding: 14px 16px;
83 |
84 | border-bottom: 2px solid var(--orange);
85 |
86 | .label {
87 | font-size: 14px;
88 | padding: 0 7px;
89 | font-weight: 600;
90 | }
91 | .number {
92 | font-size: 12px;
93 | background: var(--ticker);
94 | padding: 2px 6px;
95 | border-radius: 24px;
96 | }
97 | }
98 |
99 | .line {
100 | display: flex;
101 | width: 200vw;
102 | border-bottom: 1px solid var(--border);
103 | margin-left: -50vw;
104 | }
105 |
106 | &.mobile {
107 | margin-top: var(--verticalPadding);
108 |
109 | .content {
110 | margin: 0 auto;
111 | }
112 |
113 | @media (min-width: 768px) {
114 | display: none;
115 | }
116 | }
117 | &.desktop {
118 | display: none;
119 |
120 | @media (min-width: 768px) {
121 | display: unset;
122 |
123 | .wrapper {
124 | display: flex;
125 | margin: 0 auto;
126 | max-width: 1280px;
127 | }
128 |
129 | .offset {
130 | width: 25%;
131 | margin-right: var(--horizontalPadding);
132 | }
133 | }
134 | }
135 | `;
136 |
--------------------------------------------------------------------------------
/src/pages/Repo/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Link, useParams } from 'react-router-dom';
3 |
4 | import {
5 | Container,
6 | Breadcrumb,
7 | RepoIcon,
8 | Stats,
9 | StarIcon,
10 | ForkIcon,
11 | LinkButton,
12 | GithubIcon,
13 | } from './styles';
14 |
15 | import { APIRepo } from '../../@types';
16 |
17 | interface Data {
18 | repo?: APIRepo;
19 | error?: string;
20 | }
21 |
22 | const Repo: React.FC = () => {
23 | const { username, reponame } = useParams();
24 | const [data, setData] = useState();
25 |
26 | useEffect(() => {
27 | fetch(`https://api.github.com/repos/${username}/${reponame}`).then(
28 | async (response) => {
29 | setData(
30 | response.status === 404
31 | ? { error: 'Repository not found!' }
32 | : { repo: await response.json() }
33 | );
34 | }
35 | );
36 | }, [reponame, username]);
37 |
38 | if (data?.error) {
39 | return {data.error}
;
40 | }
41 |
42 | if (!data?.repo) {
43 | return Loading...
;
44 | }
45 |
46 | return (
47 |
48 |
49 |
50 |
51 |
52 | {username}
53 |
54 |
55 | /
56 |
57 |
58 | {reponame}
59 |
60 |
61 |
62 | {data.repo.description}
63 |
64 |
65 |
66 |
67 | {data.repo.stargazers_count}
68 | stars
69 |
70 |
71 |
72 | {data.repo.forks}
73 | forks
74 |
75 |
76 |
77 |
78 |
79 | View on GitHub
80 |
81 |
82 | );
83 | };
84 |
85 | export default Repo;
86 |
--------------------------------------------------------------------------------
/src/pages/Repo/styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 | import { RiBookMarkLine, RiStarLine } from 'react-icons/ri';
3 | import { AiOutlineFork } from 'react-icons/ai';
4 | import { FaGithub } from 'react-icons/fa';
5 |
6 | export const Container = styled.div`
7 | display: flex;
8 | flex-direction: column;
9 | padding: 16px;
10 |
11 | > p {
12 | font-size: 16px;
13 | }
14 | `;
15 |
16 | export const Breadcrumb = styled.div`
17 | margin-bottom: 16px;
18 |
19 | display: flex;
20 | align-items: center;
21 | flex-wrap: nowrap;
22 | white-space: nowrap;
23 |
24 | font-size: 18px;
25 |
26 | > a {
27 | color: var(--link);
28 | text-decoration: none;
29 |
30 | &:hover,
31 | &:focus {
32 | text-decoration: underline;
33 | }
34 |
35 | &.username {
36 | margin-left: 8px;
37 | }
38 |
39 | &.reponame {
40 | font-weight: 600;
41 | }
42 | }
43 | > span {
44 | padding: 0 5px;
45 | }
46 | `;
47 |
48 | const iconCSS = css`
49 | width: 16px;
50 | height: 16px;
51 | fill: var(--icon);
52 | flex-shrink: 0;
53 | `;
54 |
55 | export const RepoIcon = styled(RiBookMarkLine)`
56 | ${iconCSS}
57 | `;
58 |
59 | export const Stats = styled.ul`
60 | margin-top: 16px;
61 | display: flex;
62 | align-items: center;
63 |
64 | > li {
65 | display: flex;
66 | align-items: center;
67 | margin-right: 9px;
68 |
69 | > * {
70 | margin-right: 7px;
71 | color: var(--gray);
72 | }
73 | }
74 | `;
75 |
76 | export const StarIcon = styled(RiStarLine)`
77 | ${iconCSS}
78 | `;
79 |
80 | export const ForkIcon = styled(AiOutlineFork)`
81 | ${iconCSS}
82 | `;
83 |
84 | export const LinkButton = styled.a`
85 | text-decoration: none;
86 |
87 | margin-top: 24px;
88 | background: var(--gray-dark);
89 | padding: 12px 17px;
90 | border-radius: 24px;
91 | width: max-content;
92 |
93 | display: flex;
94 | align-items: center;
95 |
96 | > span {
97 | color: var(--primary);
98 | }
99 | > svg {
100 | fill: var(--primary);
101 | margin-right: 10px;
102 | }
103 | `;
104 |
105 | export const GithubIcon = styled(FaGithub)`
106 | ${iconCSS}
107 | `;
108 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/styles/GlobalStyles.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 |
3 | export default createGlobalStyle`
4 | * {
5 | margin: 0;
6 | padding: 0;
7 | box-sizing: border-box;
8 | }
9 | html {
10 | min-height: 100%;
11 | background: var(--primary);
12 | }
13 | *, button, input {
14 | border: 0;
15 | background: none;
16 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
17 | color: var(--black);
18 |
19 | transition: color .2s ease-out;
20 | }
21 | ul {
22 | list-style: none;
23 | }
24 | :root {
25 | ${(props) => {
26 | const theme = props.theme;
27 |
28 | let append = '';
29 | Object.entries(theme).forEach(([prop, value]) => {
30 | append += `--${prop}: ${value};`;
31 | });
32 |
33 | return append;
34 | }}
35 | }
36 | `;
37 |
--------------------------------------------------------------------------------
/src/styles/themes.ts:
--------------------------------------------------------------------------------
1 | export const themes = {
2 | light: {
3 | primary: '#fff',
4 | black: '#1b1f23',
5 | gray: '#586069',
6 | 'gray-light': '#6a737d',
7 | 'gray-dark': '#24292e',
8 | orange: '#f9826c',
9 |
10 | header: '#24292e',
11 | logo: '#fff',
12 | username: '#666',
13 | search: 'rgba(255, 255, 255, 0.13)',
14 | 'search-placeholder': 'hsla(0,0%,100%,.75)',
15 | icon: '#6a737d',
16 | link: '#0366d6',
17 | border: '#e1e4e8',
18 | ticker: 'rgba(209,213,218,.5)',
19 |
20 | 'calendar-scale-0': '#ebedf0',
21 | 'calendar-scale-1': '#9BE9A8',
22 | 'calendar-scale-2': '#3FC463',
23 | 'calendar-scale-3': '#30A14E',
24 | 'calendar-scale-4': '#216E3A',
25 |
26 | javascript: '#f1e05a',
27 | typescript: '#2b7489',
28 | 'other-language': '#8257e5',
29 | },
30 | dark: {
31 | primary: '#1D1D1D',
32 | black: '#c6c6c6',
33 | gray: '#afafaf',
34 | 'gray-light': '#6a737d',
35 | 'gray-dark': '#c6c6c6',
36 | orange: '#fff',
37 |
38 | header: '#181818',
39 | logo: '#fff',
40 | username: '#9b9b9b',
41 | search: '#151515',
42 | 'search-placeholder': '#c6c6c6',
43 | icon: '#9b9b9b',
44 | link: 'rgb(79, 140, 201)',
45 | border: '#343434',
46 | ticker: 'rgba(90, 90, 90, .5)',
47 |
48 | 'calendar-scale-0': '#282828',
49 | 'calendar-scale-1': 'rgba(79, 140, 201, 0.25)',
50 | 'calendar-scale-2': 'rgba(79, 140, 201, 0.5)',
51 | 'calendar-scale-3': 'rgba(79, 140, 201, 0.75)',
52 | 'calendar-scale-4': 'rgba(79, 140, 201, 1)',
53 |
54 | javascript: '#f1e05a',
55 | typescript: '#2b7489',
56 | 'other-language': '#8257e5',
57 | },
58 | };
59 |
60 | export type ThemeName = keyof typeof themes;
61 | export type ThemeType = typeof themes.light | typeof themes.dark;
62 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------