remove wrapped, remove 'Dashboard' button

This commit is contained in:
martin-mfg
2025-11-09 14:17:40 +01:00
parent 9f9051fc61
commit fabe9376cf
40 changed files with 11 additions and 2323 deletions
-3
View File
@@ -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

-77
View File
@@ -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>
+1 -3
View File
@@ -5,6 +5,4 @@ import {
deleteAccount,
} from './user';
import { getWrapped } from './wrapped';
export { setUserKey, authenticate, getUserMetadata, deleteAccount, getWrapped };
export { setUserKey, authenticate, getUserMetadata, deleteAccount };
-29
View File
@@ -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 };
+2 -10
View File
@@ -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';
+2 -10
View File
@@ -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;
+3 -31
View File
@@ -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 -2
View File
@@ -1,4 +1,3 @@
import AppTrends from './AppTrends';
import AppWrapped from './AppWrapped';
export { AppTrends, AppWrapped };
export { AppTrends };
-130
View File
@@ -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;
+2 -60
View File
@@ -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;
}
}