remove wrapped, remove 'Dashboard' button
This commit is contained in:
@@ -59,11 +59,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"setup-trends": "cp ./public/trends.html ./public/index.html",
|
||||
"setup-wrapped": "cp ./public/wrapped.html ./public/index.html",
|
||||
"start-trends": "yarn setup-trends && REACT_APP_MODE=trends craco start",
|
||||
"start-wrapped": "yarn setup-wrapped && REACT_APP_MODE=wrapped PORT=3001 craco start",
|
||||
"build-trends": "yarn setup-trends && REACT_APP_MODE=trends craco build",
|
||||
"build-wrapped": "yarn setup-wrapped && REACT_APP_MODE=wrapped craco build",
|
||||
"test": "craco test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 156 KiB |
@@ -1,77 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="title" content="Github Wrapped" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Reflect on your year of coding with GitHub Wrapped. See lines of code written, broken down by language, repository, time of day, and more!"
|
||||
/>
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://githubwrapped.io/" />
|
||||
<meta property="og:title" content="Github Wrapped" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Reflect on your year of coding with GitHub Wrapped. See lines of code written, broken down by language, repository, time of day, and more!"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://www.githubwrapped.io/preview_wrapped.png"
|
||||
/>
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:creator" content="@avgupta456" />
|
||||
<meta name="twitter:url" content="https://githubwrapped.io" />
|
||||
<meta name="twitter:title" content="Github Wrapped" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Reflect on your year of coding with GitHub Wrapped. See lines of code written, broken down by language, repository, time of day, and more!"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://www.githubwrapped.io/preview_wrapped.png"
|
||||
/>
|
||||
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-FT096DX9CK"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-FT096DX9CK');
|
||||
</script>
|
||||
|
||||
<title>GitHub Wrapped</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,6 +5,4 @@ import {
|
||||
deleteAccount,
|
||||
} from './user';
|
||||
|
||||
import { getWrapped } from './wrapped';
|
||||
|
||||
export { setUserKey, authenticate, getUserMetadata, deleteAccount, getWrapped };
|
||||
export { setUserKey, authenticate, getUserMetadata, deleteAccount };
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/* eslint-disable no-return-await */
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { BACKEND_URL } from '../constants';
|
||||
|
||||
const URL_PREFIX = `${BACKEND_URL}/wrapped`;
|
||||
|
||||
const getValidUser = async (userId) => {
|
||||
try {
|
||||
const fullUrl = `${URL_PREFIX}/valid/${userId}`;
|
||||
const result = await axios.get(fullUrl);
|
||||
return result.data.data;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getWrapped = async (userId, year) => {
|
||||
try {
|
||||
const fullUrl = `${URL_PREFIX}/${userId}?year=${year}`;
|
||||
const result = await axios.get(fullUrl);
|
||||
return result.data.data;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export { getWrapped, getValidUser };
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 187 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 194 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 185 KiB |
@@ -1,59 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { classnames } from '../../utils';
|
||||
|
||||
const WrappedSection = (props) => {
|
||||
return (
|
||||
<div className="w-full h-auto flex flex-wrap mb-8">
|
||||
{props.useTitle && (
|
||||
<p className="w-screen p-2 text-2xl lg:text-3xl">{props.title}</p>
|
||||
)}
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WrappedSection.propTypes = {
|
||||
useTitle: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
WrappedSection.defaultProps = {
|
||||
useTitle: true,
|
||||
title: '',
|
||||
};
|
||||
|
||||
const WrappedCard = (props) => {
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full p-1"
|
||||
onMouseOver={props.onMouseOver}
|
||||
onMouseOut={props.onMouseOut}
|
||||
>
|
||||
<div
|
||||
className={classnames(
|
||||
'shadow rounded-sm bg-gray-100 w-full h-full p-4 flex flex-col justify-center',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WrappedCard.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
onMouseOver: PropTypes.func,
|
||||
onMouseOut: PropTypes.func,
|
||||
};
|
||||
|
||||
WrappedCard.defaultProps = {
|
||||
className: '',
|
||||
onMouseOver: () => {},
|
||||
onMouseOut: () => {},
|
||||
};
|
||||
|
||||
export { WrappedSection, WrappedCard };
|
||||
@@ -1,145 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { WrappedCard } from '../Organization';
|
||||
import { BarGraph } from '../Templates';
|
||||
|
||||
const monthNames = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
|
||||
const dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
];
|
||||
|
||||
const BarMonth = ({ data, downloadLoading }) => {
|
||||
const newData = data?.month_data?.months || [];
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [displayContribs, setDisplayContribs] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="h-96 w-full">
|
||||
<WrappedCard>
|
||||
<div className="flex">
|
||||
<div className="flex-grow">
|
||||
<p className="text-xl font-semibold">Contributions by Month</p>
|
||||
<p>
|
||||
{displayContribs ? 'By Contribution Count' : 'By LOC Changed'}
|
||||
</p>
|
||||
</div>
|
||||
{!downloadLoading && (
|
||||
<div className="flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center"
|
||||
onClick={() => setDisplayContribs(!displayContribs)}
|
||||
>
|
||||
<span>Toggle</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{displayContribs ? (
|
||||
<BarGraph
|
||||
data={newData}
|
||||
labels={monthNames}
|
||||
xTitle="Month"
|
||||
type="contribs"
|
||||
getLabel={(d) => d.contribs}
|
||||
legendText="Contributions"
|
||||
/>
|
||||
) : (
|
||||
<BarGraph
|
||||
data={newData}
|
||||
labels={monthNames}
|
||||
xTitle="Month"
|
||||
type="loc_changed"
|
||||
getLabel={(d) => d.formatted_loc_changed.split(' ')[0]}
|
||||
legendText="LOC Changed"
|
||||
/>
|
||||
)}
|
||||
</WrappedCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BarMonth.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
downloadLoading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const BarDay = ({ data, downloadLoading }) => {
|
||||
const newData = data?.day_data?.days || [];
|
||||
|
||||
const [displayContribs, setDisplayContribs] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="h-96 w-full">
|
||||
<WrappedCard>
|
||||
<div className="flex">
|
||||
<div className="flex-grow">
|
||||
<p className="text-xl font-semibold">Contributions by Day</p>
|
||||
<p>
|
||||
{displayContribs ? 'By Contribution Count' : 'By LOC Changed'}
|
||||
</p>
|
||||
</div>
|
||||
{!downloadLoading && (
|
||||
<div className="flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center"
|
||||
onClick={() => setDisplayContribs(!displayContribs)}
|
||||
>
|
||||
<span>Toggle</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{displayContribs ? (
|
||||
<BarGraph
|
||||
data={newData}
|
||||
labels={dayNames}
|
||||
xTitle="Day"
|
||||
type="contribs"
|
||||
getLabel={(d) => d.contribs}
|
||||
legendText="Contributions"
|
||||
/>
|
||||
) : (
|
||||
<BarGraph
|
||||
data={newData}
|
||||
labels={dayNames}
|
||||
xTitle="Day"
|
||||
type="loc_changed"
|
||||
getLabel={(d) => d.formatted_loc_changed.split(' ')[0]}
|
||||
legendText="LOC Changed"
|
||||
/>
|
||||
)}
|
||||
</WrappedCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BarDay.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
downloadLoading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export { BarMonth, BarDay };
|
||||
@@ -1,125 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ResponsiveCalendar } from '@nivo/calendar';
|
||||
|
||||
import { Input } from '../../Generic';
|
||||
import { theme, scale } from '../Templates/theme';
|
||||
import { WrappedCard } from '../Organization';
|
||||
|
||||
const Calendar = ({
|
||||
data,
|
||||
startDate,
|
||||
endDate,
|
||||
highlightDays,
|
||||
highlightColors,
|
||||
downloadLoading,
|
||||
}) => {
|
||||
const newData = data?.calendar_data?.days || [];
|
||||
|
||||
const valueOptions = [
|
||||
{ value: 'contribs', label: 'Contributions', disabled: false },
|
||||
{ value: 'commits', label: 'Commits', disabled: false },
|
||||
{ value: 'issues', label: 'Issues', disabled: false },
|
||||
{ value: 'prs', label: 'Pull Requests', disabled: false },
|
||||
{ value: 'reviews', label: 'Reviews', disabled: false },
|
||||
];
|
||||
|
||||
const [value, setValue] = React.useState(valueOptions[0]);
|
||||
|
||||
const numEvents = Array.isArray(newData)
|
||||
? newData.reduce((acc, x) => acc + x[value.value], 0)
|
||||
: 0;
|
||||
|
||||
let c = 0;
|
||||
const max = Math.max(...newData.map((x) => x[value.value]));
|
||||
const quantiles = [
|
||||
Math.floor(max * 0.25),
|
||||
Math.floor(max * 0.5),
|
||||
Math.floor(max * 0.75),
|
||||
max,
|
||||
];
|
||||
|
||||
const colorScaleFn = (x) => {
|
||||
const count = (c % 365) + 1;
|
||||
c += 1;
|
||||
|
||||
const myColorScale = highlightDays.includes(count)
|
||||
? highlightColors
|
||||
: scale;
|
||||
|
||||
if (x === 0) {
|
||||
return myColorScale[0];
|
||||
}
|
||||
if (x <= quantiles[0]) {
|
||||
return myColorScale[1];
|
||||
}
|
||||
if (x <= quantiles[1]) {
|
||||
return myColorScale[2];
|
||||
}
|
||||
if (x <= quantiles[2]) {
|
||||
return myColorScale[3];
|
||||
}
|
||||
return myColorScale[4];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<WrappedCard>
|
||||
<div className="h-6 flex justify-between items-center">
|
||||
<p className="text-lg lg:text-xl font-semibold">
|
||||
Contribution Calendar
|
||||
</p>
|
||||
{!downloadLoading && (
|
||||
<Input
|
||||
className="hidden lg:block w-48 border-2 border-gray-300"
|
||||
options={valueOptions}
|
||||
selectedOption={value}
|
||||
setSelectedOption={setValue}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col h-48">
|
||||
<p className="lg:text-lg">{`${numEvents} ${value.label}`}</p>
|
||||
{Array.isArray(newData) && newData.length > 0 ? (
|
||||
<ResponsiveCalendar
|
||||
theme={theme}
|
||||
data={newData.map((item) => ({
|
||||
day: item.day,
|
||||
value: item[value.value],
|
||||
}))}
|
||||
from={startDate}
|
||||
to={endDate}
|
||||
emptyColor="#EBEDF0"
|
||||
colors={['#9BE9A8', '#40C463', '#30A14E', '#216E39']}
|
||||
margin={{ top: 10, right: 0, bottom: 0, left: 0 }}
|
||||
monthBorderColor="#ffffff"
|
||||
dayBorderWidth={2}
|
||||
dayBorderColor="#ffffff"
|
||||
colorScale={colorScaleFn}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
No data to show
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</WrappedCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Calendar.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
startDate: PropTypes.string.isRequired,
|
||||
endDate: PropTypes.string.isRequired,
|
||||
highlightDays: PropTypes.arrayOf(PropTypes.number),
|
||||
highlightColors: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
downloadLoading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
Calendar.defaultProps = {
|
||||
highlightDays: [],
|
||||
};
|
||||
|
||||
export default Calendar;
|
||||
@@ -1,109 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { WrappedCard } from '../Organization';
|
||||
|
||||
const numericPropTypes = {
|
||||
num: PropTypes.any,
|
||||
label: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const numericDefaultProps = {
|
||||
num: 'N/A',
|
||||
};
|
||||
|
||||
const NumericPlusLOC = ({ num, label }) => {
|
||||
return (
|
||||
<WrappedCard>
|
||||
<p className="text-2xl 2xl:text-3xl 3xl:text-4xl font-bold w-full text-center text-green-600">{`+${num}`}</p>
|
||||
<p className="2xl:text-lg w-full text-center text-green-600">{label}</p>
|
||||
</WrappedCard>
|
||||
);
|
||||
};
|
||||
|
||||
NumericPlusLOC.propTypes = numericPropTypes;
|
||||
NumericPlusLOC.defaultProps = numericDefaultProps;
|
||||
|
||||
const NumericMinusLOC = ({ num, label }) => {
|
||||
return (
|
||||
<WrappedCard>
|
||||
<p className="text-2xl 2xl:text-3xl 3xl:text-4xl font-bold w-full text-center text-red-600">{`-${num}`}</p>
|
||||
<p className="2xl:text-lg w-full text-center text-red-600">{label}</p>
|
||||
</WrappedCard>
|
||||
);
|
||||
};
|
||||
|
||||
NumericMinusLOC.propTypes = numericPropTypes;
|
||||
NumericMinusLOC.defaultProps = numericDefaultProps;
|
||||
|
||||
const NumericBothLOC = ({ num1, num2, label }) => {
|
||||
return (
|
||||
<WrappedCard>
|
||||
<div className="flex justify-center">
|
||||
<p className="text-2xl 2xl:text-3xl 3xl:text-4xl font-bold text-center text-green-600">
|
||||
{num1}
|
||||
</p>
|
||||
<p className="text-2xl 2xl:text-3xl 3xl:text-4xl font-bold mx-2">/</p>
|
||||
<p className="text-2xl 2xl:text-3xl 3xl:text-4xl font-bold text-center text-red-600">
|
||||
{num2}
|
||||
</p>
|
||||
</div>
|
||||
<p className="2xl:text-lg w-full text-center">{label}</p>
|
||||
</WrappedCard>
|
||||
);
|
||||
};
|
||||
|
||||
NumericBothLOC.propTypes = {
|
||||
num1: PropTypes.any,
|
||||
num2: PropTypes.any,
|
||||
label: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
NumericBothLOC.defaultProps = {
|
||||
num1: 'N/A',
|
||||
num2: 'N/A',
|
||||
};
|
||||
|
||||
const NumericBestDay = ({
|
||||
num,
|
||||
date,
|
||||
label,
|
||||
className,
|
||||
onMouseOver,
|
||||
onMouseOut,
|
||||
}) => {
|
||||
return (
|
||||
<WrappedCard
|
||||
className={className}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseOut={onMouseOut}
|
||||
>
|
||||
<div className="h-24 mb-2 flex flex-col justify-center">
|
||||
<p className="text-2xl 2xl:text-3xl 3xl:text-4xl font-bold text-center text-green-600">
|
||||
{num} Contributions
|
||||
</p>
|
||||
<p className="text-2xl 2xl:text-3xl 3xl:text-4xl font-bold text-center">
|
||||
on {date}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-lg 2xl:text-xl w-full text-center">{label}</p>
|
||||
</WrappedCard>
|
||||
);
|
||||
};
|
||||
|
||||
NumericBestDay.propTypes = {
|
||||
num: PropTypes.number.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
onMouseOver: PropTypes.func,
|
||||
onMouseOut: PropTypes.func,
|
||||
};
|
||||
|
||||
NumericBestDay.defaultProps = {
|
||||
className: '',
|
||||
onMouseOver: () => {},
|
||||
onMouseOut: () => {},
|
||||
};
|
||||
|
||||
export { NumericPlusLOC, NumericMinusLOC, NumericBothLOC, NumericBestDay };
|
||||
@@ -1,98 +0,0 @@
|
||||
/* eslint-disable react/jsx-curly-newline */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { PieChart } from '../Templates';
|
||||
import { WrappedCard } from '../Organization';
|
||||
|
||||
const PieLangs = ({ data, downloadLoading }) => {
|
||||
const [useLOCAdded, setUseLOCAdded] = React.useState(false);
|
||||
|
||||
const metric = useLOCAdded ? 'added' : 'changed';
|
||||
const newData = data?.lang_data?.[`langs_${metric}`] || [];
|
||||
|
||||
return (
|
||||
<div className="h-96 w-full">
|
||||
<WrappedCard>
|
||||
<div className="flex">
|
||||
<div className="flex-grow">
|
||||
<p className="text-xl font-semibold">Most Used Languages</p>
|
||||
<p>{useLOCAdded ? 'By LOC Added' : 'By LOC Changed'}</p>
|
||||
</div>
|
||||
{!downloadLoading && (
|
||||
<div className="flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center"
|
||||
onClick={() => setUseLOCAdded(!useLOCAdded)}
|
||||
>
|
||||
<span>Toggle</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PieChart
|
||||
data={newData}
|
||||
getArcLinkLabel={(e) => e.data.label}
|
||||
getFormattedValue={(e) => e.formatted_value}
|
||||
colors={{ datum: 'data.color' }}
|
||||
/>
|
||||
</WrappedCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PieLangs.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
downloadLoading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const PieRepos = ({ data, downloadLoading }) => {
|
||||
const [useLOCAdded, setUseLOCAdded] = React.useState(false);
|
||||
|
||||
const metric = useLOCAdded ? 'added' : 'changed';
|
||||
const newData = data?.repo_data?.[`repos_${metric}`] || [];
|
||||
|
||||
return (
|
||||
<div className="h-96 w-full">
|
||||
<WrappedCard>
|
||||
<div className="flex">
|
||||
<div className="flex-grow">
|
||||
<p className="text-xl font-semibold">Most Active Repositories</p>
|
||||
<p>{useLOCAdded ? 'By LOC Added' : 'By LOC Changed'}</p>
|
||||
</div>
|
||||
{!downloadLoading && (
|
||||
<div className="flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center"
|
||||
onClick={() => setUseLOCAdded(!useLOCAdded)}
|
||||
>
|
||||
<span>Toggle</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PieChart
|
||||
data={newData}
|
||||
getArcLinkLabel={({ data: { label } }) => {
|
||||
if (label && label.includes('/')) {
|
||||
return label.split('/')[1].replace('repository', 'private');
|
||||
}
|
||||
return label;
|
||||
}}
|
||||
getFormattedValue={(e) => e.formatted_value}
|
||||
colors={{ scheme: 'category10' }}
|
||||
/>
|
||||
</WrappedCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PieRepos.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
downloadLoading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export { PieLangs, PieRepos };
|
||||
@@ -1,59 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ResponsiveRadar } from '@nivo/radar';
|
||||
|
||||
import { WrappedCard } from '../Organization';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const Radar = ({ data }) => {
|
||||
const commits = data?.numeric_data?.contribs?.commits || 0;
|
||||
const issues = data?.numeric_data?.contribs?.issues || 0;
|
||||
const prs = data?.numeric_data?.contribs?.prs || 0;
|
||||
const reviews = data?.numeric_data?.contribs?.reviews || 0;
|
||||
|
||||
const tempData = [
|
||||
{
|
||||
name: 'Commits',
|
||||
count: Math.log(1 + commits),
|
||||
},
|
||||
{
|
||||
name: 'Issues',
|
||||
count: Math.log(1 + issues),
|
||||
},
|
||||
{
|
||||
name: 'Pull Requests',
|
||||
count: Math.log(1 + prs),
|
||||
},
|
||||
{
|
||||
name: 'Reviews',
|
||||
count: Math.log(1 + reviews),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-96 w-full">
|
||||
<WrappedCard>
|
||||
<p className="text-xl font-semibold">Contributions by Type</p>
|
||||
<p>Log Scale</p>
|
||||
<ResponsiveRadar
|
||||
data={tempData}
|
||||
keys={['count']}
|
||||
indexBy="name"
|
||||
valueFormat={(d) => Math.round(Math.exp(d) - 1)}
|
||||
margin={{ top: 30, right: 50, bottom: 30, left: 60 }}
|
||||
dotSize={10}
|
||||
colors={{ scheme: 'category10' }}
|
||||
blendMode="multiply"
|
||||
motionConfig="wobbly"
|
||||
/>
|
||||
</WrappedCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Radar.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Radar;
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { SwarmPlot } from '../Templates';
|
||||
|
||||
const formatYAxis = (value) => {
|
||||
if (value === 3600 * 12) {
|
||||
return 'Noon';
|
||||
}
|
||||
if (value === 3600 * 24) {
|
||||
return 'Midnight';
|
||||
}
|
||||
let hours = Math.floor(value / 3600);
|
||||
const suffix = hours % 24 >= 12 ? 'PM' : 'AM';
|
||||
hours = hours % 12 === 0 ? 12 : hours % 12;
|
||||
const minutes = String(Math.floor((value % 3600) / 60 / 10) * 10);
|
||||
const displayHour = String(hours).padStart(2, '0');
|
||||
const displayMinute = String(minutes).padStart(2, '0');
|
||||
return `${displayHour}:${displayMinute} ${suffix}`;
|
||||
};
|
||||
|
||||
const SwarmDay = ({ data }) => {
|
||||
let newData = data?.timestamp_data?.contribs || [];
|
||||
newData = newData.map((d, i) => {
|
||||
return {
|
||||
...d,
|
||||
groupById: 0,
|
||||
id: i,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<SwarmPlot
|
||||
header="Contributions by Time"
|
||||
data={newData}
|
||||
groupBy="groupById"
|
||||
groups={[0]}
|
||||
legend=""
|
||||
formatXAxis={() => ''}
|
||||
formatYAxis={formatYAxis}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
SwarmDay.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export { SwarmDay };
|
||||
@@ -1,7 +0,0 @@
|
||||
import Calendar from './Calendar';
|
||||
|
||||
export * from './Bar';
|
||||
export * from './Numeric';
|
||||
export * from './Pie';
|
||||
export * from './Swarm';
|
||||
export { Calendar };
|
||||
@@ -1,80 +0,0 @@
|
||||
/* eslint-disable react/jsx-curly-newline */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ResponsiveBar } from '@nivo/bar';
|
||||
|
||||
import { theme } from './theme';
|
||||
|
||||
const BarGraph = ({ data, labels, xTitle, type, getLabel, legendText }) => {
|
||||
const maxData = Math.max(...data.map((d) => d[type]));
|
||||
const minData = Math.min(
|
||||
...data.filter((d) => d.index < 11).map((d) => d[type]),
|
||||
);
|
||||
|
||||
const getColor = (d) => {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
return d.value === maxData
|
||||
? '#2BA02C'
|
||||
: d.value === minData
|
||||
? '#D62728'
|
||||
: '#468CBF';
|
||||
};
|
||||
|
||||
if (!(Array.isArray(data) && data.length > 0)) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
No data to show
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveBar
|
||||
theme={theme}
|
||||
colors={getColor}
|
||||
data={data}
|
||||
indexBy="index"
|
||||
keys={[type]}
|
||||
margin={{ top: 30, right: 0, bottom: 40, left: 80 }}
|
||||
padding={0.3}
|
||||
layout="vertical"
|
||||
axisTop={null}
|
||||
axisRight={null}
|
||||
axisBottom={{
|
||||
tickSize: 5,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend: xTitle,
|
||||
legendPosition: 'middle',
|
||||
legendOffset: 32,
|
||||
format: (value) => labels[value],
|
||||
}}
|
||||
axisLeft={{
|
||||
tickSize: 5,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend: legendText,
|
||||
legendPosition: 'middle',
|
||||
legendOffset: -60,
|
||||
}}
|
||||
label={(d) => getLabel(d.data)}
|
||||
labelSkipWidth={12}
|
||||
labelSkipHeight={12}
|
||||
labelTextColor="#fff"
|
||||
tooltip={() => null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
BarGraph.propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
labels: PropTypes.array.isRequired,
|
||||
xTitle: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
getLabel: PropTypes.func.isRequired,
|
||||
legendText: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default BarGraph;
|
||||
@@ -1,102 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ResponsivePie } from '@nivo/pie';
|
||||
|
||||
import { WrappedCard } from '../Organization';
|
||||
|
||||
const Numeric = ({ num, label }) => {
|
||||
return (
|
||||
<WrappedCard>
|
||||
<p className="text-2xl 2xl:text-3xl 3xl:text-4xl font-bold w-full text-center">
|
||||
{num || 'N/A'}
|
||||
</p>
|
||||
<p className="2xl:text-lg w-full text-center">{label}</p>
|
||||
</WrappedCard>
|
||||
);
|
||||
};
|
||||
|
||||
Numeric.propTypes = {
|
||||
num: PropTypes.any,
|
||||
label: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
Numeric.defaultProps = {
|
||||
num: 'N/A',
|
||||
};
|
||||
|
||||
const NumericOutOf = ({
|
||||
num,
|
||||
outOf,
|
||||
format,
|
||||
label,
|
||||
color,
|
||||
className,
|
||||
onMouseOver,
|
||||
onMouseOut,
|
||||
}) => {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const CenteredMetric = ({ dataWithArc, centerX, centerY }) => {
|
||||
let total = 0;
|
||||
// eslint-disable-next-line react/prop-types
|
||||
dataWithArc.forEach((datum) => {
|
||||
total += datum.id === '1' ? datum.value : 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<text
|
||||
x={centerX}
|
||||
y={centerY}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
className="text-2xl 2xl:text-3xl font-bold"
|
||||
>
|
||||
{format(total)}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<WrappedCard
|
||||
className={className}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseOut={onMouseOut}
|
||||
>
|
||||
<div className="w-full h-24 mb-2">
|
||||
<ResponsivePie
|
||||
data={[
|
||||
{ id: '1', value: num, color },
|
||||
{ id: '2', value: outOf - num, color: '#d1d5db' },
|
||||
]}
|
||||
innerRadius={0.8}
|
||||
enableArcLabels={false}
|
||||
enableArcLinkLabels={false}
|
||||
colors={{ datum: 'data.color' }}
|
||||
layers={['arcs', CenteredMetric]}
|
||||
tooltip={() => null}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-lg 2xl:text-xl w-full text-center">{label}</p>
|
||||
</WrappedCard>
|
||||
);
|
||||
};
|
||||
|
||||
NumericOutOf.propTypes = {
|
||||
num: PropTypes.number.isRequired,
|
||||
outOf: PropTypes.number.isRequired,
|
||||
format: PropTypes.func,
|
||||
label: PropTypes.string.isRequired,
|
||||
color: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
onMouseOver: PropTypes.func,
|
||||
onMouseOut: PropTypes.func,
|
||||
};
|
||||
|
||||
NumericOutOf.defaultProps = {
|
||||
format: (x) => x,
|
||||
color: '#30A14E',
|
||||
className: '',
|
||||
onMouseOver: () => {},
|
||||
onMouseOut: () => {},
|
||||
};
|
||||
|
||||
export { Numeric, NumericOutOf };
|
||||
@@ -1,68 +0,0 @@
|
||||
/* eslint-disable react/jsx-curly-newline */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ResponsivePie } from '@nivo/pie';
|
||||
|
||||
import { theme } from './theme';
|
||||
|
||||
const PieChart = ({ data, getArcLinkLabel, getFormattedValue, colors }) => {
|
||||
if (!(Array.isArray(data) && data.length > 0)) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
No data to show
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ResponsivePie
|
||||
theme={theme}
|
||||
data={data}
|
||||
margin={{ top: 20, right: 40, bottom: 20, left: 40 }}
|
||||
innerRadius={0.4}
|
||||
padAngle={0.7}
|
||||
cornerRadius={3}
|
||||
activeOuterRadiusOffset={8}
|
||||
borderWidth={1}
|
||||
borderColor={{ from: 'color', modifiers: [['darker', 0.2]] }}
|
||||
// Arc Link Settings
|
||||
arcLinkLabel={(e) => getArcLinkLabel(e)}
|
||||
arcLinkLabelsSkipAngle={45}
|
||||
arcLinkLabelsTextOffset={0}
|
||||
arcLinkLabelsTextColor={{ from: 'color' }}
|
||||
arcLinkLabelsDiagonalLength={5}
|
||||
arcLinkLabelsStraightLength={5}
|
||||
arcLinkLabelsThickness={0}
|
||||
// Arc Label Settings
|
||||
arcLabel={(e) => getFormattedValue(e.data)}
|
||||
arcLabelsSkipAngle={45}
|
||||
arcLabelsTextColor="#fff"
|
||||
// Tooltip
|
||||
tooltip={({ datum }) => (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
padding: 6,
|
||||
color: datum.color,
|
||||
background: '#fff',
|
||||
boxShadow: '0px 0px 10px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
>
|
||||
<strong>{datum.label}</strong>
|
||||
{`: ${getFormattedValue(datum.data)}`}
|
||||
</div>
|
||||
)}
|
||||
colors={colors}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
PieChart.propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
getArcLinkLabel: PropTypes.func.isRequired,
|
||||
getFormattedValue: PropTypes.func.isRequired,
|
||||
colors: PropTypes.any.isRequired,
|
||||
};
|
||||
|
||||
export default PieChart;
|
||||
@@ -1,94 +0,0 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable react/jsx-curly-newline */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ResponsiveSwarmPlot } from '@nivo/swarmplot';
|
||||
|
||||
import { theme } from './theme';
|
||||
import { WrappedCard } from '../Organization';
|
||||
|
||||
const MemoizedResponsiveSwarmPlot = React.memo(
|
||||
ResponsiveSwarmPlot,
|
||||
(prevProps, nextProps) => prevProps.data?.length === nextProps.data?.length,
|
||||
);
|
||||
|
||||
const SwarmPlot = ({
|
||||
header,
|
||||
data,
|
||||
groupBy,
|
||||
groups,
|
||||
legend,
|
||||
formatXAxis,
|
||||
formatYAxis,
|
||||
}) => {
|
||||
const tickValues = [0, 1, 2, 3, 4, 5, 6, 7, 8].map((i) => 10800 * i);
|
||||
|
||||
return (
|
||||
<div className="w-full h-96">
|
||||
<WrappedCard>
|
||||
<p className="text-lg lg:text-xl font-semibold">{header}</p>
|
||||
<p>{`${data.length} Sampled Contributions, Eastern Time`}</p>
|
||||
{Array.isArray(data) && data.length > 0 ? (
|
||||
<MemoizedResponsiveSwarmPlot
|
||||
theme={theme}
|
||||
isInteractive={false}
|
||||
animate={false}
|
||||
data={data}
|
||||
groupBy={groupBy}
|
||||
groups={groups}
|
||||
identity="id"
|
||||
value="timestamp"
|
||||
size={6}
|
||||
forceStrength={4}
|
||||
simulationIterations={60}
|
||||
colors={{ scheme: 'category10' }}
|
||||
gridYValues={tickValues}
|
||||
valueScale={{ type: 'linear', min: 0, max: 86400, reverse: true }}
|
||||
margin={{ top: 20, right: 0, bottom: 20, left: 70 }}
|
||||
axisTop={null}
|
||||
axisRight={null}
|
||||
axisBottom={{
|
||||
orient: 'bottom',
|
||||
tickSize: 10,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend,
|
||||
legendPosition: 'middle',
|
||||
legendOffset: 46,
|
||||
format: (value) => formatXAxis(value),
|
||||
}}
|
||||
axisLeft={{
|
||||
orient: 'left',
|
||||
tickSize: 10,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend: 'Time of Day',
|
||||
legendPosition: 'middle',
|
||||
legendOffset: -86,
|
||||
tickValues,
|
||||
format: (value) => formatYAxis(value),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
No data to show
|
||||
</div>
|
||||
)}
|
||||
</WrappedCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SwarmPlot.propTypes = {
|
||||
header: PropTypes.string.isRequired,
|
||||
data: PropTypes.array.isRequired,
|
||||
groupBy: PropTypes.string.isRequired,
|
||||
groups: PropTypes.array.isRequired,
|
||||
legend: PropTypes.string.isRequired,
|
||||
formatXAxis: PropTypes.func.isRequired,
|
||||
formatYAxis: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SwarmPlot;
|
||||
@@ -1,7 +0,0 @@
|
||||
import BarGraph from './Bar';
|
||||
import PieChart from './Pie';
|
||||
import SwarmPlot from './Swarm';
|
||||
|
||||
export * from './Numeric';
|
||||
export * from './theme';
|
||||
export { BarGraph, PieChart, SwarmPlot };
|
||||
@@ -1,20 +0,0 @@
|
||||
export const theme = {
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Segoe UI',
|
||||
};
|
||||
|
||||
export const scale = ['#EBEDF0', '#9BE9A8', '#40C463', '#30A14E', '#216E39'];
|
||||
export const hoverScale = [
|
||||
'#A6C9F5',
|
||||
'#7EC7D1',
|
||||
'#50B5AF',
|
||||
'#48A3A4',
|
||||
'#418A9A',
|
||||
];
|
||||
export const singleHoverScale = [
|
||||
'#468CBF',
|
||||
'#468CBF',
|
||||
'#468CBF',
|
||||
'#468CBF',
|
||||
'#468CBF',
|
||||
];
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './Organization';
|
||||
export * from './Templates';
|
||||
export * from './Specifics';
|
||||
@@ -3,6 +3,5 @@ import Preview from './Preview';
|
||||
export * from './Generic';
|
||||
export * from './Card';
|
||||
export * from './Home';
|
||||
export * from './Wrapped';
|
||||
|
||||
export { Preview };
|
||||
|
||||
@@ -12,20 +12,12 @@ export const HOST = PROD
|
||||
: 'localhost:3000';
|
||||
|
||||
export const REDIRECT_URI = PROD
|
||||
? MODE === 'trends'
|
||||
? `https://${HOST}/frontend/user`
|
||||
: 'https://www.githubtrends.io/user/wrapped'
|
||||
: MODE === 'trends'
|
||||
? `http://${HOST}/frontend/user`
|
||||
: 'http://localhost:3000/user/wrapped';
|
||||
? `https://${HOST}/frontend/user`
|
||||
: `http://${HOST}/frontend/user`;
|
||||
|
||||
export const GITHUB_PRIVATE_AUTH_URL = `https://github.com/login/oauth/authorize?scope=user,repo&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}/private`;
|
||||
export const GITHUB_PUBLIC_AUTH_URL = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}/public`;
|
||||
|
||||
export const WRAPPED_URL = PROD
|
||||
? 'https://www.githubwrapped.io'
|
||||
: 'http://localhost:3001';
|
||||
|
||||
export const BACKEND_URL = PROD
|
||||
? 'https://api.githubtrends.io'
|
||||
: 'http://localhost:8000';
|
||||
|
||||
@@ -4,7 +4,7 @@ import ReactDOM from 'react-dom/client';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import configureStore from './redux/store';
|
||||
import { AppTrends, AppWrapped } from './pages/App';
|
||||
import { AppTrends } from './pages/App';
|
||||
import './index.css';
|
||||
|
||||
import { MODE } from './constants';
|
||||
@@ -19,15 +19,7 @@ if (MODE === 'trends') {
|
||||
<AppTrends />
|
||||
</Provider>,
|
||||
);
|
||||
} else if (MODE === 'wrapped') {
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<AppWrapped />
|
||||
</Provider>,
|
||||
);
|
||||
} else {
|
||||
// Throw an error if the mode is not set correctly.
|
||||
throw new Error(
|
||||
'REACT_APP_MODE must be set to "trends" or "wrapped" in your .env file.',
|
||||
);
|
||||
throw new Error('REACT_APP_MODE must be set to "trends" in your .env file.');
|
||||
}
|
||||
|
||||
@@ -14,42 +14,13 @@ import {
|
||||
|
||||
import Header from './Header';
|
||||
import LandingScreen from '../Landing';
|
||||
import DemoScreen from '../Demo';
|
||||
import { SignUpScreen } from '../Auth';
|
||||
import HomeScreen from '../Home';
|
||||
import SettingsScreen from '../Settings';
|
||||
import { NoMatchScreen } from '../Misc';
|
||||
import { getUserMetadata } from '../../api';
|
||||
import { WRAPPED_URL } from '../../constants';
|
||||
import Footer from './Footer';
|
||||
|
||||
function WrappedAuthRedirectScreen() {
|
||||
// for wrapped auth redirects
|
||||
const { rest } = useParams();
|
||||
useEffect(() => {
|
||||
const code = new URLSearchParams(window.location.search).get('code');
|
||||
window.location.href = `${WRAPPED_URL}/${rest}?code=${code}`;
|
||||
}, [rest]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function WrappedRedirectScreen() {
|
||||
// redirects /wrapped/* to https://www.githubwrapped.com/*
|
||||
const { userId, year } = useParams();
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
if (year) {
|
||||
window.location.href = `${WRAPPED_URL}/${userId}/${year}`;
|
||||
} else {
|
||||
window.location.href = `${WRAPPED_URL}/${userId}`;
|
||||
}
|
||||
} else {
|
||||
window.location.href = `${WRAPPED_URL}/`;
|
||||
}
|
||||
}, [userId, year]);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const userId = useSelector((state) => state.user.userId);
|
||||
const userKey = useSelector((state) => state.user.userKey);
|
||||
@@ -82,23 +53,8 @@ function App() {
|
||||
{!isAuthenticated && (
|
||||
<Route path="/signup" element={<SignUpScreen />} />
|
||||
)}
|
||||
<Route path="/demo" element={<DemoScreen />} />
|
||||
<Route
|
||||
path="/user/wrapped/:rest"
|
||||
element={<WrappedAuthRedirectScreen />}
|
||||
/>
|
||||
<Route path="/user/*" element={<HomeScreen />} />
|
||||
<Route
|
||||
path="/wrapped/:userId/:year"
|
||||
element={<WrappedRedirectScreen />}
|
||||
/>
|
||||
<Route
|
||||
path="/wrapped/:userId"
|
||||
element={<WrappedRedirectScreen />}
|
||||
/>
|
||||
<Route path="/wrapped" element={<WrappedRedirectScreen />} />
|
||||
<Route path="/settings" element={<SettingsScreen />} />
|
||||
<Route path="/:userId" element={<WrappedRedirectScreen />} />
|
||||
<Route exact path="/" element={<LandingScreen />} />
|
||||
<Route path="*" element={<NoMatchScreen />} />
|
||||
</Routes>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
|
||||
import Header from './Header';
|
||||
import { SignUpScreen } from '../Auth';
|
||||
import { SelectUserScreen, WrappedScreen } from '../Wrapped';
|
||||
import { NoMatchScreen } from '../Misc';
|
||||
|
||||
import { setUserAccess as _setPrivateAccess } from '../../redux/actions/userActions';
|
||||
import { getUserMetadata } from '../../api';
|
||||
import Footer from './Footer';
|
||||
|
||||
function App() {
|
||||
const userId = useSelector((state) => state.user.userId);
|
||||
const isAuthenticated = userId && userId.length > 0;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const setPrivateAccess = (access) => dispatch(_setPrivateAccess(access));
|
||||
|
||||
useEffect(() => {
|
||||
async function getPrivateAccess() {
|
||||
if (userId && userId.length > 0) {
|
||||
const result = await getUserMetadata(userId);
|
||||
if (result !== null && result.private_access !== undefined) {
|
||||
setPrivateAccess(result.private_access);
|
||||
}
|
||||
}
|
||||
}
|
||||
getPrivateAccess();
|
||||
}, [userId]);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col">
|
||||
<Router>
|
||||
<Header mode="wrapped" />
|
||||
<section className="bg-white text-gray-700 flex-grow">
|
||||
<Routes>
|
||||
{!isAuthenticated && (
|
||||
<Route path="/signup" element={<SignUpScreen />} />
|
||||
)}
|
||||
<Route path="/" element={<SelectUserScreen />} />
|
||||
<Route path="/public/" element={<SelectUserScreen />} />
|
||||
<Route path="/private/" element={<SelectUserScreen />} />
|
||||
<Route path="/:userId/:year" element={<WrappedScreen />} />
|
||||
<Route path="/:userId" element={<WrappedScreen />} />
|
||||
<Route path="*" element={<NoMatchScreen />} />
|
||||
</Routes>
|
||||
</section>
|
||||
<Footer />
|
||||
</Router>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -10,7 +10,7 @@ import { MdSettings as SettingsIcon } from 'react-icons/md';
|
||||
import { logout as _logout } from '../../redux/actions/userActions';
|
||||
import appIcon from '../../assets/appLogo64.png';
|
||||
import { classnames } from '../../utils';
|
||||
import { GITHUB_PUBLIC_AUTH_URL, WRAPPED_URL } from '../../constants';
|
||||
import { GITHUB_PUBLIC_AUTH_URL } from '../../constants';
|
||||
|
||||
const propTypes = {
|
||||
to: PropTypes.string.isRequired,
|
||||
@@ -79,26 +79,7 @@ const Header = ({ mode }) => {
|
||||
{mode === 'trends' && (
|
||||
<span className="ml-2 text-xl">GitHub Trends</span>
|
||||
)}
|
||||
{mode === 'wrapped' && (
|
||||
<span className="ml-2 text-xl">GitHub Wrapped</span>
|
||||
)}
|
||||
</Link>
|
||||
{/* Pages: Wrapped, Dashboard, Demo */}
|
||||
{mode === 'trends' && (
|
||||
<div className="hidden md:flex">
|
||||
<Link
|
||||
to={WRAPPED_URL}
|
||||
className="px-4 py-1 mr-3 rounded-sm bg-blue-500 hover:bg-blue-600 text-white"
|
||||
>
|
||||
Wrapped
|
||||
</Link>
|
||||
{isAuthenticated ? (
|
||||
<StandardLink to="/user">Dashboard</StandardLink>
|
||||
) : (
|
||||
<StandardLink to="/demo">Demo</StandardLink>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Auth Pages: Sign Up, Log In, Log Out */}
|
||||
<div className="hidden md:flex ml-auto items-center text-base justify-center">
|
||||
{isAuthenticated ? (
|
||||
@@ -144,18 +125,9 @@ const Header = ({ mode }) => {
|
||||
<div className={classnames('p-5 pt-0', !toggle && 'hidden')}>
|
||||
{mode === 'trends' && (
|
||||
<>
|
||||
<MobileLink to={WRAPPED_URL} onClick={() => setToggle(false)}>
|
||||
Wrapped
|
||||
<MobileLink to="/user" onClick={() => setToggle(false)}>
|
||||
Dashboard
|
||||
</MobileLink>
|
||||
{isAuthenticated ? (
|
||||
<MobileLink to="/user" onClick={() => setToggle(false)}>
|
||||
Dashboard
|
||||
</MobileLink>
|
||||
) : (
|
||||
<MobileLink to="/demo" onClick={() => setToggle(false)}>
|
||||
Demo
|
||||
</MobileLink>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isAuthenticated ? (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import AppTrends from './AppTrends';
|
||||
import AppWrapped from './AppWrapped';
|
||||
|
||||
export { AppTrends, AppWrapped };
|
||||
export { AppTrends };
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Button, SvgInline } from '../../components';
|
||||
import { HOST } from '../../constants';
|
||||
import { classnames } from '../../utils';
|
||||
|
||||
const DemoScreen = () => {
|
||||
const [userName, setUserName] = useState('');
|
||||
const [selectedUserName, setSelectedUserName] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
let userNameInput;
|
||||
|
||||
useEffect(() => {
|
||||
userNameInput.focus();
|
||||
}, [userNameInput]);
|
||||
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true);
|
||||
setSelectedUserName(userName);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const firstCardUrl =
|
||||
selectedUserName.length > 0
|
||||
? `https://${HOST}/api?username=${selectedUserName}&client=demo`
|
||||
: `https://${HOST}/api?username=anuraghazra&client=demo`;
|
||||
|
||||
const secondCardUrl =
|
||||
selectedUserName.length > 0
|
||||
? `https://${HOST}/api/top-langs?username=${selectedUserName}&client=demo`
|
||||
: `https://${HOST}/api/top-langs?username=anuraghazra&client=demo`;
|
||||
|
||||
return (
|
||||
<div className="h-full py-8 flex flex-col xl:flex-row justify-center items-center">
|
||||
<div className="h-full w-full px-5 pb-5 xl:w-1/3 xl:pl-8 xl:pr-0 xl:pb-0">
|
||||
<div className="h-full w-full bg-gray-100 rounded-sm p-4 shadow">
|
||||
<h1 className="text-2xl font-bold text-gray-800 text-center mb-4">
|
||||
GitHub Trends Demo
|
||||
</h1>
|
||||
<p className="text-center text-sm text-gray-600">
|
||||
This is a demo of the GitHub Trends API. Enter your GitHub username
|
||||
to see statistics about your top languages and repositories from the
|
||||
past month.
|
||||
</p>
|
||||
<div className="form-control my-8">
|
||||
<p>Enter your GitHub username to get started!</p>
|
||||
<div className="flex space-x-2 mt-2">
|
||||
<input
|
||||
type="text"
|
||||
ref={(input) => {
|
||||
userNameInput = input;
|
||||
}}
|
||||
placeholder="Enter Username"
|
||||
className={classnames(
|
||||
'bg-white text-gray-700 w-full input input-bordered rounded-sm',
|
||||
error && 'input-error',
|
||||
)}
|
||||
onChange={(e) => {
|
||||
setUserName(e.target.value);
|
||||
setError('');
|
||||
}}
|
||||
onKeyPress={async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Go
|
||||
</Button>
|
||||
</div>
|
||||
{error ? (
|
||||
<div className="text-red-500 text-sm mt-2">
|
||||
<strong>Error:</strong> {error}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm mt-2 py-5" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-center text-sm text-red-500">
|
||||
This demo uses a public access token that is heavily rate limited.
|
||||
For full customization, private contributions, and a personal access
|
||||
token, create an account instead!
|
||||
</p>
|
||||
<div className="flex justify-center mt-8">
|
||||
<Button className="text-white bg-blue-500 hover:bg-blue-600">
|
||||
<Link to="/signup">Create an Account</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full xl:w-2/3 px-5 xl:px-8">
|
||||
<div className="h-full w-full bg-gray-100 rounded-sm p-4 shadow">
|
||||
<h1 className="text-2xl font-bold text-gray-800 text-center mb-4">
|
||||
{selectedUserName === ''
|
||||
? 'Enter a Username'
|
||||
: `Example Cards for ${selectedUserName}`}
|
||||
</h1>
|
||||
<div className="w-full flex flex-wrap">
|
||||
<div className="w-full lg:w-1/2 p-2">
|
||||
<SvgInline
|
||||
className="w-full h-full"
|
||||
url={firstCardUrl}
|
||||
forceLoading={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full lg:w-1/2 p-2">
|
||||
<SvgInline
|
||||
className="w-full h-full"
|
||||
url={secondCardUrl}
|
||||
forceLoading={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DemoScreen;
|
||||
@@ -1,3 +0,0 @@
|
||||
import DemoScreen from './Demo';
|
||||
|
||||
export default DemoScreen;
|
||||
@@ -9,12 +9,10 @@ import { FaCheck as CheckIcon, FaGithub as GithubIcon } from 'react-icons/fa';
|
||||
import { Button, Preview } from '../../components';
|
||||
|
||||
import mockup from '../../assets/mockup.png';
|
||||
import wrapped from '../../assets/wrapped1.png';
|
||||
import avgupta456Langs from '../../assets/avgupta456_langs.png';
|
||||
import tiangoloRepos from '../../assets/tiangolo_repos.png';
|
||||
import reininkRepos from '../../assets/reinink_repos.png';
|
||||
import dhermesLangs from '../../assets/dhermes_langs.png';
|
||||
import { WRAPPED_URL } from '../../constants';
|
||||
|
||||
function LandingScreen() {
|
||||
const userId = useSelector((state) => state.user.userId);
|
||||
@@ -93,9 +91,9 @@ function LandingScreen() {
|
||||
</p>
|
||||
<br />
|
||||
<div>
|
||||
<Link to={isAuthenticated ? '/user' : '/demo'} className="w-auto">
|
||||
<Link to="/user" className="w-auto">
|
||||
<Button className="my-4 mr-4 w-auto justify-center text-white text-xl 3xl:text-2xl bg-gray-700 hover:bg-gray-800">
|
||||
{isAuthenticated ? 'Visit Dashboard' : 'Try the Demo'}
|
||||
Visit Dashboard
|
||||
</Button>
|
||||
</Link>
|
||||
{!isAuthenticated && (
|
||||
@@ -108,57 +106,6 @@ function LandingScreen() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-gray-700 w-full flex flex-wrap items-center py-4 px-4">
|
||||
<div className="w-full lg:w-1/2 3xl:w-1/3 mx-auto p-8 flex flex-col">
|
||||
<h1 className="text-4xl text-gray-900 font-medium mb-12">
|
||||
Reflect on your year
|
||||
<div>
|
||||
with <strong>GitHub Wrapped</strong>
|
||||
</div>
|
||||
</h1>
|
||||
<p className="text-lg font-bold">1. Detailed</p>
|
||||
<p>
|
||||
GitHub Wrapped provides a breakdown of your contributions by date,
|
||||
by date, time, repository, and language. Over 20 stats are
|
||||
displayed.
|
||||
</p>
|
||||
<br />
|
||||
<p className="text-lg font-bold">2. Visual</p>
|
||||
<p>
|
||||
Understand your coding contributions like never before with an
|
||||
interactive calendar, bar charts, pie charts, and more.
|
||||
</p>
|
||||
<br />
|
||||
<p className="text-lg font-bold">3. Public</p>
|
||||
<p>
|
||||
Share your GitHub Wrapped link with your friends and colleagues and
|
||||
take a look at their contributions too.{' '}
|
||||
<strong>No account required</strong>, although rate limiting may
|
||||
apply.
|
||||
</p>
|
||||
<br />
|
||||
<div>
|
||||
<Link to={`${WRAPPED_URL}/avgupta456`} className="w-auto">
|
||||
<Button className="my-4 mr-4 w-auto justify-center text-white text-xl 3xl:text-2xl bg-gray-700 hover:bg-gray-800">
|
||||
Example
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
to={isAuthenticated ? `${WRAPPED_URL}/${userId}` : WRAPPED_URL}
|
||||
className="w-auto"
|
||||
>
|
||||
<Button className="my-4 mr-4 w-auto justify-center text-white text-xl 3xl:text-2xl bg-blue-500 hover:bg-blue-600">
|
||||
Get your Wrapped
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-1/2 p-8">
|
||||
<div className="flex justify-center">
|
||||
<img src={wrapped} alt="preview" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-200 text-gray-700 w-full flex flex-col justify-center items-center pt-16 py-4 px-4">
|
||||
<h1 className="text-4xl font-medium mb-4">GitHub Trends</h1>
|
||||
<h2 className="w-2/3 text-center text-lg">
|
||||
@@ -211,11 +158,6 @@ function LandingScreen() {
|
||||
<p className="text-3xl font-medium">Ready to get started?</p>
|
||||
<p className="text-3xl font-medium">Create an account today.</p>
|
||||
<div className="mt-2">
|
||||
<Link to="/demo" className="w-auto">
|
||||
<Button className="my-4 mr-4 w-auto justify-center text-white text-xl 3xl:text-2xl bg-gray-700 hover:bg-gray-800">
|
||||
Try Demo
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/signup" className="w-auto">
|
||||
<Button className="my-4 mr-4 w-auto justify-center text-white text-xl 3xl:text-2xl bg-blue-500 hover:bg-blue-600">
|
||||
Sign Up
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
/* eslint-disable no-alert */
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { ClipLoader } from 'react-spinners';
|
||||
import { BsInfoCircle } from 'react-icons/bs';
|
||||
import { FaGithub as GithubIcon, FaCheck as CheckIcon } from 'react-icons/fa';
|
||||
|
||||
import { getValidUser } from '../../api/wrapped';
|
||||
import { Button, Preview } from '../../components';
|
||||
import { classnames, sleep } from '../../utils';
|
||||
import wrapped1 from '../../assets/wrapped1.png';
|
||||
import wrapped2 from '../../assets/wrapped2.png';
|
||||
import wrapped3 from '../../assets/wrapped3.png';
|
||||
import { PROD } from '../../constants';
|
||||
import { authenticate, setUserKey } from '../../api';
|
||||
import { login as _login } from '../../redux/actions/userActions';
|
||||
|
||||
const SelectUserScreen = () => {
|
||||
const userId = useSelector((state) => state.user.userId || '');
|
||||
|
||||
const [userName, setUserName] = useState(userId);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
let userNameInput;
|
||||
|
||||
useEffect(() => {
|
||||
userNameInput.focus();
|
||||
}, [userNameInput]);
|
||||
|
||||
const login = (newUserId, userKey) => dispatch(_login(newUserId, userKey));
|
||||
|
||||
useEffect(() => {
|
||||
async function redirectCode() {
|
||||
// After requesting Github access, Github redirects back to your app with a code parameter
|
||||
const url = window.location.href;
|
||||
|
||||
if (url.includes('error=')) {
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
// If Github API returns the code parameter
|
||||
if (url.includes('code=')) {
|
||||
const tempPrivateAccess = url.includes('private');
|
||||
const newUrl = url.split('?code=');
|
||||
const subStr = PROD ? 'githubwrapped.io' : 'localhost:3001';
|
||||
const redirect = `${url.split(subStr)[0]}${subStr}/`;
|
||||
window.history.pushState({}, null, redirect);
|
||||
const userKey = await setUserKey(newUrl[1]);
|
||||
const newUserId = await authenticate(newUrl[1], tempPrivateAccess);
|
||||
login(newUserId, userKey);
|
||||
}
|
||||
}
|
||||
|
||||
redirectCode();
|
||||
}, []);
|
||||
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsLoading(true);
|
||||
const validUser = await getValidUser(userName);
|
||||
if (validUser.includes('Valid user')) {
|
||||
const newUserName = validUser.split(' ')[2];
|
||||
await sleep(10);
|
||||
navigate(`/${newUserName}`);
|
||||
} else if (validUser === 'GitHub user not found') {
|
||||
setError('GitHub user not found. Check your spelling and try again.');
|
||||
} else if (validUser === 'Repo not starred') {
|
||||
setError(
|
||||
'This user has not starred the GitHub Trends repository. Please star the repo and try again.',
|
||||
);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full -mt-16 text-white">
|
||||
<div className="w-full h-full bg-blue-500 pt-24 pb-8">
|
||||
<div className="w-full text-center p-8 lg:pb-2">
|
||||
<h1 className="text-2xl md:text-3xl lg:text-4xl font-medium mb-2">
|
||||
Reflect on your year <br className="inline sm:hidden" />
|
||||
with <strong>GitHub Wrapped</strong>
|
||||
</h1>
|
||||
<p className="hidden sm:inline text-lg">
|
||||
Powered by{' '}
|
||||
<strong>
|
||||
<Link
|
||||
to="https://www.githubtrends.io"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub Trends
|
||||
</Link>
|
||||
</strong>{' '}
|
||||
(not affiliated with GitHub)
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full h-full flex flex-wrap items-center container mx-auto">
|
||||
<div className="w-full lg:w-1/2 xl:w-2/5 flex flex-col items-center">
|
||||
<div className="p-6 m-2 md:w-80 lg:w-96 rounded-lg bg-gray-200 shadow text-gray-800">
|
||||
<div className="text-sm lg:text-lg mb-4 flex items-center">
|
||||
<p>
|
||||
<strong>Step 1</strong>: Star the GitHub repository.{' '}
|
||||
</p>
|
||||
<div
|
||||
className="hidden md:inline md:tooltip"
|
||||
data-tip="This helps prevent spam requests and protect user privacy. Feel free to unstar after."
|
||||
>
|
||||
<BsInfoCircle className="h-4 w-4 ml-2 text-gray-500 hover:text-gray-800 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col items-center">
|
||||
<a
|
||||
href="https://www.github.com/avgupta456/github-trends"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button className="bg-black text-white flex items-center">
|
||||
Star on
|
||||
<GithubIcon className="ml-1.5 w-5 h-5" />
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm lg:text-lg mt-8 mb-4">
|
||||
<strong>Step 2</strong>: Enter your GitHub username!
|
||||
</p>
|
||||
<div className="flex space-x-2 mt-2">
|
||||
<input
|
||||
type="text"
|
||||
autoCapitalize="none"
|
||||
ref={(input) => {
|
||||
userNameInput = input;
|
||||
}}
|
||||
placeholder="Enter Username"
|
||||
className={classnames(
|
||||
'bg-white text-gray-700 w-full input input-bordered rounded-sm',
|
||||
error && 'input-error',
|
||||
)}
|
||||
defaultValue={userName}
|
||||
onChange={(e) => {
|
||||
setUserName(e.target.value);
|
||||
setError('');
|
||||
}}
|
||||
onKeyPress={async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white flex items-center"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ClipLoader size={22} color="#fff" speedMultiplier={0.5} />
|
||||
) : (
|
||||
'Go'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{error ? (
|
||||
<div className="text-red-500 text-sm mt-2">
|
||||
<strong>Error:</strong> {error}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm mt-2 py-5" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-1/2 xl:w-3/5 lg:px-8 flex flex-col items-center">
|
||||
<div className="w-full xl:w-4/5 2xl:w-3/4 mx-auto">
|
||||
<Preview
|
||||
pages={[wrapped1, wrapped2, wrapped3]}
|
||||
details={[
|
||||
'Detailed metrics and insightful charts',
|
||||
'Lines of code metrics (by langs and repos)',
|
||||
'Over a dozen stats to reflect on your year',
|
||||
]}
|
||||
showArrows={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white text-gray-700 w-full py-16">
|
||||
<div className="w-full container mx-auto flex flex-col items-center">
|
||||
<h1 className="text-4xl font-medium mb-4">See some examples</h1>
|
||||
<div className="w-full flex flex-wrap justify-center items-center px-4">
|
||||
{[
|
||||
{
|
||||
name: 'Linus Torvalds',
|
||||
username: 'torvalds',
|
||||
url: 'https://avatars.githubusercontent.com/u/1024025?v=4',
|
||||
blurb: 'Creator of Linux',
|
||||
},
|
||||
{
|
||||
name: 'Evan You',
|
||||
username: 'yyx990803',
|
||||
url: 'https://avatars.githubusercontent.com/u/499550?v=4',
|
||||
blurb: 'Creator of Vue',
|
||||
},
|
||||
{
|
||||
name: 'shadcn',
|
||||
username: 'shadcn',
|
||||
url: 'https://avatars.githubusercontent.com/u/124599?v=4',
|
||||
blurb: 'Vercel, shadcn/ui',
|
||||
},
|
||||
{
|
||||
name: 'Sindre Sorhus',
|
||||
username: 'sindresorhus',
|
||||
url: 'https://avatars.githubusercontent.com/u/170270?v=4',
|
||||
blurb: 'Open-sourcer',
|
||||
},
|
||||
].map((user) => (
|
||||
<div className="w-full md:w-1/2 lg:w-1/4 p-4" key={user.username}>
|
||||
<Link to={`/${user.username}`}>
|
||||
<div className="w-full rounded bg-gray-50 hover:bg-gray-100 shadow p-4 flex">
|
||||
<img
|
||||
src={user.url}
|
||||
alt={user.username}
|
||||
className="w-24 h-24 rounded-full mr-4 my-auto"
|
||||
/>
|
||||
<div className="w-full flex flex-col items-center">
|
||||
<strong className="w-full text-center">
|
||||
{user.name}
|
||||
</strong>
|
||||
<p className="w-full text-center">{user.blurb}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-200 text-gray-800 w-full pt-16">
|
||||
<div className="w-full container mx-auto flex flex-col items-center justify-center">
|
||||
<h1 className="text-4xl font-medium mb-4">GitHub Trends</h1>
|
||||
<h2 className="w-3/4 text-center text-sm lg:text-lg">
|
||||
GitHub Trends dives deep into the GitHub API to bring you insightful
|
||||
metrics and visualizations. We access individual commits to compute
|
||||
accurate and granular statistics.
|
||||
</h2>
|
||||
<div className="w-4/5 mx-auto py-8 flex flex-wrap items-center justify-center">
|
||||
{[
|
||||
{
|
||||
header: 'Measures Contribs',
|
||||
text: 'Calculates your stats on a per-contribution level, allowing for deeper insights',
|
||||
},
|
||||
{
|
||||
header: 'LOC Insights',
|
||||
text: 'See aggregate stats on lines of code (LOC) written across all contributions',
|
||||
},
|
||||
{
|
||||
header: 'Language Breakdowns',
|
||||
text: 'Showcase your favorite languages with LOC language breakdowns',
|
||||
},
|
||||
{
|
||||
header: 'Private Mode',
|
||||
text: 'Use a PAT to avoid rate limiting and include private contributions',
|
||||
},
|
||||
{
|
||||
header: 'Exciting Visualizations',
|
||||
text: 'Visualize your contributions with bar graphs, pie charts, and more',
|
||||
},
|
||||
{
|
||||
header: 'Shareable Stats',
|
||||
text: 'Easily add your cards to your GitHub and share them online',
|
||||
},
|
||||
].map((item, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div className="flex w-full md:w-1/2 lg:w-1/3 p-4" key={index}>
|
||||
<div className="w-4 h-4 mt-1 mr-2">
|
||||
<CheckIcon className="w-full h-full text-green-600" />
|
||||
</div>
|
||||
<div className="w-4/5 flex flex-col justify-top">
|
||||
<p className="text-xl mb-1 font-medium">{item.header}</p>
|
||||
<p>{item.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectUserScreen;
|
||||
@@ -1,323 +0,0 @@
|
||||
/* eslint-disable react/jsx-one-expression-per-line */
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { toPng } from 'html-to-image';
|
||||
import download from 'downloadjs';
|
||||
import { FaArrowLeft as LeftArrowIcon } from 'react-icons/fa';
|
||||
import { BsImage as ImageIcon, BsInfoCircle } from 'react-icons/bs';
|
||||
import Select from 'react-select';
|
||||
import { ClipLoader } from 'react-spinners';
|
||||
|
||||
import { getWrapped } from '../../api';
|
||||
import {
|
||||
WrappedSection,
|
||||
Numeric,
|
||||
NumericOutOf,
|
||||
Calendar,
|
||||
hoverScale,
|
||||
singleHoverScale,
|
||||
BarMonth,
|
||||
BarDay,
|
||||
PieLangs,
|
||||
PieRepos,
|
||||
SwarmDay,
|
||||
NumericPlusLOC,
|
||||
NumericMinusLOC,
|
||||
NumericBothLOC,
|
||||
NumericBestDay,
|
||||
} from '../../components';
|
||||
import Radar from '../../components/Wrapped/Specifics/Radar';
|
||||
import { LoadingScreen } from './sections';
|
||||
import { classnames } from '../../utils';
|
||||
import { CURR_YEAR } from '../../constants';
|
||||
|
||||
const WrappedScreen = () => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { userId, year } = useParams();
|
||||
year = year || `${CURR_YEAR}`;
|
||||
|
||||
const currUserId = useSelector((state) => state.user.userId);
|
||||
const usePrivate = useSelector((state) => state.user.privateAccess);
|
||||
|
||||
const [data, setData] = useState({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [highlightDays, setHighlightDays] = useState([]);
|
||||
const [highlightColors, setHighlightColors] = useState(hoverScale);
|
||||
|
||||
const [downloadLoading, setDownloadLoading] = useState(false);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const downloadImage = async () => {
|
||||
const dataUrl = await toPng(document.getElementById('screenshot-div'));
|
||||
download(dataUrl, 'github-wrapped.png');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function getData() {
|
||||
if (userId?.length > 0 && year > 2010 && year <= CURR_YEAR) {
|
||||
const output = await getWrapped(userId, year);
|
||||
if (
|
||||
output !== null &&
|
||||
output !== undefined &&
|
||||
Object.keys(output).length > 0
|
||||
) {
|
||||
setData(output);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
getData();
|
||||
}, [userId, year]);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
const startStreak = data?.numeric_data?.misc?.longest_streak_days?.[0] || 0;
|
||||
const endStreak = data?.numeric_data?.misc?.longest_streak_days?.[1] || 0;
|
||||
const startGap = data?.numeric_data?.misc?.longest_gap_days?.[0] || 0;
|
||||
const endGap = data?.numeric_data?.misc?.longest_gap_days?.[1] || 0;
|
||||
|
||||
const bestDayMonth =
|
||||
data?.numeric_data?.misc?.best_day_date?.split('-')?.[1] || '-';
|
||||
const bestDayDay =
|
||||
data?.numeric_data?.misc?.best_day_date?.split('-')?.[2] || '-';
|
||||
const bestDayYear =
|
||||
data?.numeric_data?.misc?.best_day_date?.split('-')?.[0] || '-';
|
||||
|
||||
return (
|
||||
<div className="containermx-auto">
|
||||
<div
|
||||
className={classnames(
|
||||
'h-full w-full bg-white flex flex-row flex-wrap justify-center items-center',
|
||||
'px-2 lg:px-4 xl:px-16 py-4 lg:py-8',
|
||||
)}
|
||||
id="screenshot-div"
|
||||
>
|
||||
<WrappedSection useTitle={false}>
|
||||
<div className="w-full h-auto flex flex-row flex-wrap -mb-4">
|
||||
{!downloadLoading && (
|
||||
<Link to="/">
|
||||
<LeftArrowIcon className="hidden md:block absolute ml-2 mt-2 h-8 w-8 text-gray-500 hover:text-gray-800" />
|
||||
</Link>
|
||||
)}
|
||||
<p className="text-xl font-semibold text-center w-full">
|
||||
{`${userId}'s`}
|
||||
</p>
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<Select
|
||||
options={Array.from(
|
||||
{ length: 10 },
|
||||
(_, i) => CURR_YEAR - i,
|
||||
).map((x) => ({ value: x, label: x }))}
|
||||
value={{ value: year, label: year }}
|
||||
onChange={(e) => {
|
||||
window.location.href = `/${userId}/${e.value}`;
|
||||
}}
|
||||
/>
|
||||
<p className="text-2xl md:text-3xl ml-2">GitHub Wrapped</p>
|
||||
</div>
|
||||
<div className="mt-2 text-md text-center w-full text-gray-600 flex justify-center items-center">
|
||||
Private Access:{' '}
|
||||
{userId === currUserId && usePrivate ? 'True' : 'False'}
|
||||
{!(userId === currUserId && usePrivate) && (
|
||||
<div
|
||||
className="hidden md:inline md:tooltip"
|
||||
data-tip="For private access, create an account with GitHub Trends and authenticate with GitHub."
|
||||
>
|
||||
<BsInfoCircle className="h-4 w-4 ml-2 text-gray-500 hover:text-gray-800 cursor-pointer" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{data?.incomplete && (
|
||||
<p className="mt-2 text-md text-center w-full text-red-600">
|
||||
Incomplete Data. Please refresh later to finish loading.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</WrappedSection>
|
||||
<WrappedSection title="Contribution Calendar">
|
||||
<div className="w-full lg:w-4/5">
|
||||
<Calendar
|
||||
data={data}
|
||||
startDate={`${year}-01-02`}
|
||||
endDate={`${year}-12-31`}
|
||||
highlightDays={highlightDays}
|
||||
highlightColors={highlightColors}
|
||||
downloadLoading={downloadLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2 md:w-1/4 lg:w-1/5">
|
||||
<NumericOutOf
|
||||
num={data?.numeric_data?.misc?.total_days || 0}
|
||||
outOf={365}
|
||||
label="Active Days"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2 md:w-1/4">
|
||||
<NumericOutOf
|
||||
num={data?.numeric_data?.misc?.longest_streak || 0}
|
||||
outOf={100}
|
||||
label="Longest Streak"
|
||||
className="hover:bg-gray-200 cursor-pointer"
|
||||
onMouseOver={() => {
|
||||
setHighlightDays(
|
||||
Array.from(
|
||||
{ length: endStreak - startStreak + 1 },
|
||||
(_, i) => startStreak + i,
|
||||
),
|
||||
);
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setHighlightDays([]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2 md:w-1/4">
|
||||
<NumericOutOf
|
||||
num={data?.numeric_data?.misc?.longest_gap || 0}
|
||||
outOf={100}
|
||||
label="Longest Gap"
|
||||
color="#EF4444"
|
||||
className="hover:bg-gray-200 cursor-pointer"
|
||||
onMouseOver={() =>
|
||||
setHighlightDays(
|
||||
Array.from(
|
||||
{ length: endGap - startGap + 1 },
|
||||
(_, i) => startGap + i,
|
||||
),
|
||||
)
|
||||
}
|
||||
onMouseOut={() => setHighlightDays([])}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2 md:w-1/4">
|
||||
<NumericOutOf
|
||||
num={data?.numeric_data?.misc?.weekend_percent}
|
||||
outOf={100}
|
||||
format={(x) => `${x}%`}
|
||||
label="Weekend Activity"
|
||||
color="#468CBF"
|
||||
className="hover:bg-gray-200 cursor-pointer"
|
||||
onMouseOver={() => {
|
||||
const Sunday = Array.from({ length: 55 }, (_, i) => i).map(
|
||||
(x) =>
|
||||
x * 7 -
|
||||
((year % 7) + 6) +
|
||||
Math.max(0, Math.floor((2024 - year) / 4)),
|
||||
);
|
||||
const Saturday = Array.from({ length: 55 }, (_, i) => i).map(
|
||||
(x) =>
|
||||
x * 7 -
|
||||
(year % 7) +
|
||||
Math.max(0, Math.floor((2024 - year) / 4)),
|
||||
);
|
||||
setHighlightDays([...Sunday, ...Saturday]);
|
||||
}}
|
||||
onMouseOut={() => setHighlightDays([])}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden lg:block w-1/4">
|
||||
<NumericBestDay
|
||||
num={data?.numeric_data?.misc?.best_day_count}
|
||||
date={`${bestDayMonth}/${bestDayDay}/${bestDayYear}`}
|
||||
label="Busiest Day"
|
||||
className="hover:bg-gray-200 cursor-pointer"
|
||||
onMouseOver={() => {
|
||||
setHighlightColors(singleHoverScale);
|
||||
setHighlightDays([data?.numeric_data?.misc?.best_day_index]);
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setHighlightColors(hoverScale);
|
||||
setHighlightDays([]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</WrappedSection>
|
||||
<WrappedSection title="Lines of Code (LOC) Analysis">
|
||||
<div className="w-full md:w-1/2 xl:w-1/3">
|
||||
<PieLangs data={data} downloadLoading={downloadLoading} />
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 xl:w-1/3">
|
||||
<PieRepos data={data} downloadLoading={downloadLoading} />
|
||||
</div>
|
||||
<div className="w-full xl:w-1/3 flex flex-wrap">
|
||||
<div className="w-full md:w-1/2 lg:w-1/4 xl:w-1/2">
|
||||
<NumericPlusLOC
|
||||
num={data?.numeric_data?.loc?.loc_additions}
|
||||
label="LOC Additions"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 lg:w-1/4 xl:w-1/2">
|
||||
<NumericMinusLOC
|
||||
num={data?.numeric_data?.loc?.loc_deletions}
|
||||
label="LOC Deletions"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 lg:w-1/4 xl:w-1/2">
|
||||
<NumericBothLOC
|
||||
num1={data?.numeric_data?.loc?.loc_additions_per_commit}
|
||||
num2={data?.numeric_data?.loc?.loc_deletions_per_commit}
|
||||
label="Typical Commit"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 lg:w-1/4 xl:w-1/2">
|
||||
<Numeric
|
||||
num={data?.numeric_data?.loc?.loc_changed_per_day}
|
||||
label="Lines Changed / Day"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</WrappedSection>
|
||||
<WrappedSection title="Contribution Breakdown">
|
||||
<div className="w-full lg:w-1/3">
|
||||
<Radar data={data} />
|
||||
</div>
|
||||
<div className="w-full lg:w-2/3">
|
||||
<BarMonth data={data} downloadLoading={downloadLoading} />
|
||||
</div>
|
||||
<div className="w-full lg:w-2/3">
|
||||
<BarDay data={data} downloadLoading={downloadLoading} />
|
||||
</div>
|
||||
<div className="w-full lg:w-1/3">
|
||||
<SwarmDay data={data} />
|
||||
</div>
|
||||
</WrappedSection>
|
||||
{downloadLoading && (
|
||||
<div className="text-center text-2xl md:text-3xl lg:text-4xl font-bold text-blue-500">
|
||||
Create your own at www.githubwrapped.io
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="fixed bottom-2 right-2 md:bottom-4 md:right-4 lg:bottom-8 lg:right-8">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-sm shadow bg-gray-700 hover:bg-gray-800 text-gray-50 px-3 py-2"
|
||||
onClick={() => {
|
||||
setDownloadLoading(true);
|
||||
setTimeout(() => {
|
||||
downloadImage();
|
||||
setDownloadLoading(false);
|
||||
}, 10);
|
||||
}}
|
||||
>
|
||||
{downloadLoading ? (
|
||||
<div className="w-28 h-6 flex justify-center">
|
||||
<ClipLoader size={22} color="#fff" speedMultiplier={0.5} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-28 h-6 flex items-center">
|
||||
<p>Save Image</p>
|
||||
<ImageIcon className="ml-1.5 w-5 h-5" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WrappedScreen;
|
||||
@@ -1,4 +0,0 @@
|
||||
import SelectUserScreen from './SelectUser';
|
||||
import WrappedScreen from './Wrapped';
|
||||
|
||||
export { SelectUserScreen, WrappedScreen };
|
||||
@@ -1,79 +0,0 @@
|
||||
/* eslint-disable react/jsx-one-expression-per-line */
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { PulseLoader } from 'react-spinners';
|
||||
import Typist from 'react-typist';
|
||||
import TypistLoop from 'react-typist-loop';
|
||||
|
||||
import './loading.css';
|
||||
|
||||
const LoadingScreen = () => {
|
||||
const [showLoadingMessage, setShowLoadingMessage] = useState(false);
|
||||
const [showLoadingErrorMessage, setShowLoadingErrorMessage] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setShowLoadingMessage(true);
|
||||
}, 8000);
|
||||
const timer2 = setTimeout(() => {
|
||||
setShowLoadingErrorMessage(true);
|
||||
}, 50000);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
clearTimeout(timer2);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full py-8 flex flex-col justify-center items-center">
|
||||
{showLoadingErrorMessage ? (
|
||||
<div className="w-96 bg-gray-50 shadow p-4 text-gray-700 text-center text-lg">
|
||||
Something went wrong. Please try again in a couple minutes or raise an
|
||||
issue on{' '}
|
||||
<a
|
||||
href="https://github.com/avgupta456/github-trends/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 underline"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
. Thank you!
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<PulseLoader color="#3B82F6" speedMultiplier={0.5} />
|
||||
</div>
|
||||
{showLoadingMessage ? (
|
||||
<TypistLoop interval={200}>
|
||||
{[
|
||||
'Loading your Data...',
|
||||
'Crunching Numbers...',
|
||||
'Analyzing Trends...',
|
||||
'Drawing Figures...',
|
||||
'Almost there!',
|
||||
].map((text, i) => (
|
||||
<Typist
|
||||
key={text}
|
||||
cursor={{ blink: true }}
|
||||
className="font-typist text-center text-2xl"
|
||||
>
|
||||
<Typist.Delay ms={500 * i} />
|
||||
{text}
|
||||
<Typist.Delay ms={i === 4 ? 20000 : 3000} />
|
||||
<Typist.Backspace count={text.length} />
|
||||
</Typist>
|
||||
))}
|
||||
</TypistLoop>
|
||||
) : (
|
||||
<div className="h-8" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingScreen;
|
||||
@@ -1,112 +0,0 @@
|
||||
/* eslint-disable no-else-return */
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { SquareLoader } from 'react-spinners';
|
||||
|
||||
const LoadingScreen = () => {
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
|
||||
// Should take max ~45 seconds, added extra 10 seconds to Dec wait time
|
||||
const waitTime = [
|
||||
2000, 2000, 2000, 2000, 3000, 3000, 3000, 4000, 4000, 4000, 6000, 20000,
|
||||
];
|
||||
|
||||
const [currMonth, setCurrMonth] = useState(0);
|
||||
|
||||
// increment currMonth every 5 seconds
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrMonth(currMonth + 1);
|
||||
}, waitTime[currMonth]);
|
||||
return () => clearInterval(interval);
|
||||
}, [currMonth]);
|
||||
|
||||
const getTile = (i) => {
|
||||
if (i < currMonth) {
|
||||
return (
|
||||
<div
|
||||
key={months[i]}
|
||||
className="w-16 h-16 rounded bg-blue-500 m-1.5 text-center flex flex-col justify-center"
|
||||
>
|
||||
<div className="text-white">{months[i]}</div>
|
||||
</div>
|
||||
);
|
||||
} else if (i === currMonth) {
|
||||
return (
|
||||
<SquareLoader
|
||||
key={months[i]}
|
||||
color="#3A82F6"
|
||||
speedMultiplier={0.75}
|
||||
size={64}
|
||||
className="rounded bg-blue-500 m-1.5"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
key={months[i]}
|
||||
className="w-16 h-16 rounded bg-gray-500 m-1.5 text-center flex flex-col justify-center"
|
||||
>
|
||||
<div className="text-white">{months[i]}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full py-8 flex flex-col justify-center items-center">
|
||||
{currMonth < 12 ? (
|
||||
<>
|
||||
<div>Querying the GitHub API by Months</div>
|
||||
<div className="flex flex-wrap m-4 justify-center">
|
||||
<div className="flex flex-wrap justify-center">
|
||||
<div className="flex">
|
||||
{Array.from({ length: 3 }).map((_, i) => getTile(i))}
|
||||
</div>
|
||||
<div className="flex">
|
||||
{Array.from({ length: 3 }).map((_, i) => getTile(i + 3))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-center">
|
||||
<div className="flex">
|
||||
{Array.from({ length: 3 }).map((_, i) => getTile(i + 6))}
|
||||
</div>
|
||||
<div className="flex">
|
||||
{Array.from({ length: 3 }).map((_, i) => getTile(i + 9))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-4/5 lg:w-1/3 bg-gray-50 shadow p-4 text-gray-700 text-center text-lg">
|
||||
Loading your data is taking longer than expected. Try refreshing the
|
||||
page, and if that fails, raise an issue on{' '}
|
||||
<a
|
||||
href="https://github.com/avgupta456/github-trends/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 underline"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
. Thank you for your patience!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingScreen;
|
||||
@@ -1,3 +0,0 @@
|
||||
import LoadingScreen from './Loading';
|
||||
|
||||
export { LoadingScreen };
|
||||
@@ -1,19 +0,0 @@
|
||||
.Typist .Cursor {
|
||||
display: inline-block;
|
||||
}
|
||||
.Typist .Cursor--blinking {
|
||||
opacity: 1;
|
||||
animation: blink 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user