${svg}`,
}}
/>
);
};
SvgInline.propTypes = {
className: PropTypes.any,
url: PropTypes.string.isRequired,
forceLoading: PropTypes.bool,
compact: PropTypes.bool,
};
SvgInline.defaultProps = {
className: '',
forceLoading: false,
compact: false,
};
export default SvgInline;
================================================
FILE: frontend/src/components/Card/index.js
================================================
import SvgInline from './SVG';
import { Card, Image } from './Card';
export { Card, Image, SvgInline };
================================================
FILE: frontend/src/components/Generic/Button.js
================================================
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
import PropTypes from 'prop-types';
import { classnames } from '../../utils';
const Button = (props) => {
return (
{props.children}
);
};
Button.propTypes = {
className: PropTypes.string,
children: PropTypes.node.isRequired,
};
Button.defaultProps = {
className: '',
};
export default Button;
================================================
FILE: frontend/src/components/Generic/Checkbox.js
================================================
/* eslint-disable jsx-a11y/interactive-supports-focus */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import React from 'react';
import PropTypes from 'prop-types';
const Checkbox = ({ question, variable, setVariable, disabled }) => {
return (
setVariable(!variable)}
role="button"
>
setVariable(!variable)}
/>
{question}
);
};
Checkbox.propTypes = {
question: PropTypes.string.isRequired,
variable: PropTypes.bool.isRequired,
setVariable: PropTypes.func.isRequired,
disabled: PropTypes.bool,
};
Checkbox.defaultProps = {
disabled: false,
};
export default Checkbox;
================================================
FILE: frontend/src/components/Generic/Input.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { classnames } from '../../utils';
// options is of form [{value: '', label: '', disabled: true/false}]
const Input = ({
options,
selectedOption,
setSelectedOption,
disabled,
className,
}) => {
return (
setSelectedOption(
options.find((item) => item.label === e.target.value),
// eslint-disable-next-line prettier/prettier
)}
disabled={disabled}
>
{options.map((option) => (
{option.label}
))}
);
};
Input.propTypes = {
options: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
disabled: PropTypes.bool,
}),
).isRequired,
selectedOption: PropTypes.shape({
value: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
}).isRequired,
setSelectedOption: PropTypes.func.isRequired,
disabled: PropTypes.bool,
className: PropTypes.string,
};
Input.defaultProps = {
disabled: false,
className: '',
};
export default Input;
================================================
FILE: frontend/src/components/Generic/index.js
================================================
import Button from './Button';
import Checkbox from './Checkbox';
import Input from './Input';
export { Button, Checkbox, Input };
================================================
FILE: frontend/src/components/Home/CheckboxSection.js
================================================
/* eslint-disable jsx-a11y/interactive-supports-focus */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import React from 'react';
import PropTypes from 'prop-types';
import Section from './Section';
import { Checkbox } from '../Generic';
const CheckboxSection = ({
title,
text,
question,
variable,
setVariable,
disabled,
}) => {
return (
);
};
CheckboxSection.propTypes = {
title: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
question: PropTypes.string.isRequired,
variable: PropTypes.bool.isRequired,
setVariable: PropTypes.func.isRequired,
disabled: PropTypes.bool,
};
CheckboxSection.defaultProps = {
disabled: false,
};
export default CheckboxSection;
================================================
FILE: frontend/src/components/Home/DateRangeSection.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Section from './Section';
import { Input } from '../Generic';
const DateRangeSection = ({
selectedTimeRange,
setSelectedTimeRange,
// eslint-disable-next-line no-unused-vars
privateAccess,
}) => {
const timeRangeOptions = [
{ id: 1, label: 'Past 1 Month', disabled: false, value: 'one_month' },
{
id: 2,
label: 'Past 3 Months',
disabled: false,
value: 'three_months',
},
{ id: 2, label: 'Past 6 Months', disabled: false, value: 'six_months' },
{ id: 3, label: 'Past 1 Year', disabled: false, value: 'one_year' },
// { id: 4, label: 'All Time', disabled: !privateAccess, value: 'all_time' },
{ id: 4, label: 'All Time', disabled: true, value: 'all_time' },
];
const selectedOption = selectedTimeRange || timeRangeOptions[2];
return (
);
};
DateRangeSection.propTypes = {
selectedTimeRange: PropTypes.object.isRequired,
setSelectedTimeRange: PropTypes.func.isRequired,
privateAccess: PropTypes.bool.isRequired,
};
export default DateRangeSection;
================================================
FILE: frontend/src/components/Home/Progress.js
================================================
/* eslint-disable react/no-array-index-key */
import React from 'react';
import PropTypes from 'prop-types';
import {
FaArrowLeft as LeftArrowIcon,
FaArrowRight as RightArrowIcon,
} from 'react-icons/fa';
import { classnames } from '../../utils';
const ProgressSection = ({ num, item, passed, onClick }) => {
return (
{`Step ${num + 1}`}
{item}
);
};
ProgressSection.propTypes = {
num: PropTypes.number.isRequired,
item: PropTypes.string.isRequired,
passed: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
};
const ProgressBar = ({ items, currItem, setCurrItem }) => {
const leftDisabled = currItem === 0;
const rightDisabled = currItem === items.length - 1;
return (
setCurrItem(currItem - 1)}
/>
{items.map((item, index) => {
return (
= index}
onClick={() => setCurrItem(index)}
/>
);
})}
setCurrItem(currItem + 1)}
/>
);
};
ProgressBar.propTypes = {
items: PropTypes.array.isRequired,
currItem: PropTypes.number.isRequired,
setCurrItem: PropTypes.func.isRequired,
};
export default ProgressBar;
================================================
FILE: frontend/src/components/Home/Section.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { HiOutlineLightningBolt as LightningIcon } from 'react-icons/hi';
const Section = (props) => {
return (
{props.title}
{props.children}
);
};
Section.propTypes = {
title: PropTypes.string,
children: PropTypes.node,
};
Section.defaultProps = {
title: 'Test',
children:
This is a test!
,
};
export default Section;
================================================
FILE: frontend/src/components/Home/index.js
================================================
import ProgressBar from './Progress';
import CheckboxSection from './CheckboxSection';
import DateRangeSection from './DateRangeSection';
export { ProgressBar, CheckboxSection, DateRangeSection };
================================================
FILE: frontend/src/components/Preview/Preview.js
================================================
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import {
FaArrowRight as ArrowRightIcon,
FaArrowLeft as ArrowLeftIcon,
} from 'react-icons/fa';
import { classnames } from '../../utils';
const Preview = ({ pages, details, showArrows }) => {
const totalPages = pages.length;
const [page, setPage] = useState(0);
const prevPage = () => {
setPage((page - 1 + totalPages) % totalPages);
};
const nextPage = () => {
setPage((page + 1 + totalPages) % totalPages);
};
useEffect(() => {
const interval = setInterval(nextPage, 5000);
return () => clearInterval(interval);
}, [page]);
return (
{showArrows && (
)}
{showArrows && (
)}
{details[page]}
);
};
Preview.propTypes = {
pages: PropTypes.arrayOf(PropTypes.any).isRequired,
details: PropTypes.arrayOf(PropTypes.string).isRequired,
showArrows: PropTypes.bool,
};
Preview.defaultProps = {
showArrows: true,
};
export default Preview;
================================================
FILE: frontend/src/components/Preview/index.js
================================================
import Preview from './Preview';
export default Preview;
================================================
FILE: frontend/src/components/Wrapped/Organization.js
================================================
/* eslint-disable jsx-a11y/mouse-events-have-key-events */
import React from 'react';
import PropTypes from 'prop-types';
import { classnames } from '../../utils';
const WrappedSection = (props) => {
return (
{props.useTitle && (
{props.title}
)}
{props.children}
);
};
WrappedSection.propTypes = {
useTitle: PropTypes.bool,
title: PropTypes.string,
children: PropTypes.node.isRequired,
};
WrappedSection.defaultProps = {
useTitle: true,
title: '',
};
const WrappedCard = (props) => {
return (
);
};
WrappedCard.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
onMouseOver: PropTypes.func,
onMouseOut: PropTypes.func,
};
WrappedCard.defaultProps = {
className: '',
onMouseOver: () => {},
onMouseOut: () => {},
};
export { WrappedSection, WrappedCard };
================================================
FILE: frontend/src/components/Wrapped/Specifics/Bar.js
================================================
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 (
Contributions by Month
{displayContribs ? 'By Contribution Count' : 'By LOC Changed'}
{!downloadLoading && (
setDisplayContribs(!displayContribs)}
>
Toggle
)}
{displayContribs ? (
d.contribs}
legendText="Contributions"
/>
) : (
d.formatted_loc_changed.split(' ')[0]}
legendText="LOC Changed"
/>
)}
);
};
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 (
Contributions by Day
{displayContribs ? 'By Contribution Count' : 'By LOC Changed'}
{!downloadLoading && (
setDisplayContribs(!displayContribs)}
>
Toggle
)}
{displayContribs ? (
d.contribs}
legendText="Contributions"
/>
) : (
d.formatted_loc_changed.split(' ')[0]}
legendText="LOC Changed"
/>
)}
);
};
BarDay.propTypes = {
data: PropTypes.object.isRequired,
downloadLoading: PropTypes.bool.isRequired,
};
export { BarMonth, BarDay };
================================================
FILE: frontend/src/components/Wrapped/Specifics/Calendar.js
================================================
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 (
Contribution Calendar
{!downloadLoading && (
)}
{`${numEvents} ${value.label}`}
{Array.isArray(newData) && newData.length > 0 ? (
({
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}
/>
) : (
No data to show
)}
);
};
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;
================================================
FILE: frontend/src/components/Wrapped/Specifics/Numeric.js
================================================
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 (
{`+${num}`}
{label}
);
};
NumericPlusLOC.propTypes = numericPropTypes;
NumericPlusLOC.defaultProps = numericDefaultProps;
const NumericMinusLOC = ({ num, label }) => {
return (
{`-${num}`}
{label}
);
};
NumericMinusLOC.propTypes = numericPropTypes;
NumericMinusLOC.defaultProps = numericDefaultProps;
const NumericBothLOC = ({ num1, num2, label }) => {
return (
{label}
);
};
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 (
{num} Contributions
on {date}
{label}
);
};
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 };
================================================
FILE: frontend/src/components/Wrapped/Specifics/Pie.js
================================================
/* 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 (
Most Used Languages
{useLOCAdded ? 'By LOC Added' : 'By LOC Changed'}
{!downloadLoading && (
setUseLOCAdded(!useLOCAdded)}
>
Toggle
)}
e.data.label}
getFormattedValue={(e) => e.formatted_value}
colors={{ datum: 'data.color' }}
/>
);
};
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 (
Most Active Repositories
{useLOCAdded ? 'By LOC Added' : 'By LOC Changed'}
{!downloadLoading && (
setUseLOCAdded(!useLOCAdded)}
>
Toggle
)}
{
if (label && label.includes('/')) {
return label.split('/')[1].replace('repository', 'private');
}
return label;
}}
getFormattedValue={(e) => e.formatted_value}
colors={{ scheme: 'category10' }}
/>
);
};
PieRepos.propTypes = {
data: PropTypes.object.isRequired,
downloadLoading: PropTypes.bool.isRequired,
};
export { PieLangs, PieRepos };
================================================
FILE: frontend/src/components/Wrapped/Specifics/Radar.js
================================================
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 (
Contributions by Type
Log Scale
Math.round(Math.exp(d) - 1)}
margin={{ top: 30, right: 50, bottom: 30, left: 60 }}
dotSize={10}
colors={{ scheme: 'category10' }}
blendMode="multiply"
motionConfig="wobbly"
/>
);
};
Radar.propTypes = {
data: PropTypes.object.isRequired,
};
export default Radar;
================================================
FILE: frontend/src/components/Wrapped/Specifics/Swarm.js
================================================
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 (
''}
formatYAxis={formatYAxis}
/>
);
};
SwarmDay.propTypes = {
data: PropTypes.object.isRequired,
};
export { SwarmDay };
================================================
FILE: frontend/src/components/Wrapped/Specifics/index.js
================================================
import Calendar from './Calendar';
export * from './Bar';
export * from './Numeric';
export * from './Pie';
export * from './Swarm';
export { Calendar };
================================================
FILE: frontend/src/components/Wrapped/Templates/Bar.js
================================================
/* 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 (
No data to show
);
}
return (
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;
================================================
FILE: frontend/src/components/Wrapped/Templates/Numeric.js
================================================
/* eslint-disable jsx-a11y/mouse-events-have-key-events */
import React from 'react';
import PropTypes from 'prop-types';
import { ResponsivePie } from '@nivo/pie';
import { WrappedCard } from '../Organization';
const Numeric = ({ num, label }) => {
return (
{num || 'N/A'}
{label}
);
};
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 (
{format(total)}
);
};
return (
null}
/>
{label}
);
};
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 };
================================================
FILE: frontend/src/components/Wrapped/Templates/Pie.js
================================================
/* 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 (
No data to show
);
}
return (
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 }) => (
{datum.label}
{`: ${getFormattedValue(datum.data)}`}
)}
colors={colors}
/>
);
};
PieChart.propTypes = {
data: PropTypes.array.isRequired,
getArcLinkLabel: PropTypes.func.isRequired,
getFormattedValue: PropTypes.func.isRequired,
colors: PropTypes.any.isRequired,
};
export default PieChart;
================================================
FILE: frontend/src/components/Wrapped/Templates/Swarm.js
================================================
/* 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 (
{header}
{`${data.length} Sampled Contributions, Eastern Time`}
{Array.isArray(data) && data.length > 0 ? (
formatXAxis(value),
}}
axisLeft={{
orient: 'left',
tickSize: 10,
tickPadding: 5,
tickRotation: 0,
legend: 'Time of Day',
legendPosition: 'middle',
legendOffset: -86,
tickValues,
format: (value) => formatYAxis(value),
}}
/>
) : (
No data to show
)}
);
};
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;
================================================
FILE: frontend/src/components/Wrapped/Templates/index.js
================================================
import BarGraph from './Bar';
import PieChart from './Pie';
import SwarmPlot from './Swarm';
export * from './Numeric';
export * from './theme';
export { BarGraph, PieChart, SwarmPlot };
================================================
FILE: frontend/src/components/Wrapped/Templates/theme.js
================================================
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',
];
================================================
FILE: frontend/src/components/Wrapped/index.js
================================================
export * from './Organization';
export * from './Templates';
export * from './Specifics';
================================================
FILE: frontend/src/components/index.js
================================================
import Preview from './Preview';
export * from './Generic';
export * from './Card';
export * from './Home';
export * from './Wrapped';
export { Preview };
================================================
FILE: frontend/src/constants.js
================================================
/* eslint-disable no-nested-ternary */
export const PROD = process.env.REACT_APP_PROD === 'true';
export const USE_LOGGER = true;
export const CLIENT_ID = PROD
? process.env.REACT_APP_PROD_CLIENT_ID
: process.env.REACT_APP_DEV_CLIENT_ID;
export const MODE = process.env.REACT_APP_MODE;
export const REDIRECT_URI = PROD
? MODE === 'trends'
? 'https://www.githubtrends.io/user'
: 'https://www.githubtrends.io/user/wrapped'
: MODE === 'trends'
? 'http://localhost:3000/user'
: 'http://localhost:3000/user/wrapped';
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';
export const CURR_YEAR = 2024;
================================================
FILE: frontend/src/index.css
================================================
/* ./src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: 'Segoe UI', Ubuntu, Sans-Serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
================================================
FILE: frontend/src/index.js
================================================
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import configureStore from './redux/store';
import { AppTrends, AppWrapped } from './pages/App';
import './index.css';
import { MODE } from './constants';
export const store = configureStore();
const root = ReactDOM.createRoot(document.getElementById('root'));
if (MODE === 'trends') {
root.render(
,
);
} else if (MODE === 'wrapped') {
root.render(
,
);
} 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.',
);
}
================================================
FILE: frontend/src/pages/App/AppTrends.js
================================================
/* eslint-disable jsx-a11y/anchor-is-valid */
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
BrowserRouter as Router,
Routes,
Route,
useParams,
} from 'react-router-dom';
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, RedirectScreen } from '../Misc';
import { setPrivateAccess as _setPrivateAccess } from '../../redux/actions/userActions';
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 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 (
{!isAuthenticated && (
} />
)}
} />
} />
}
/>
} />
}
/>
}
/>
} />
} />
} />
} />
} />
);
}
export default App;
================================================
FILE: frontend/src/pages/App/AppWrapped.js
================================================
/* eslint-disable jsx-a11y/anchor-is-valid */
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 { setPrivateAccess 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 (
{!isAuthenticated && (
} />
)}
} />
} />
} />
} />
} />
} />
);
}
export default App;
================================================
FILE: frontend/src/pages/App/Footer.js
================================================
import React from 'react';
import { CURR_YEAR } from '../../constants';
function Footer() {
return (
{`© ${CURR_YEAR} GitHub Trends`}
);
}
export default Footer;
================================================
FILE: frontend/src/pages/App/Header.js
================================================
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { GiHamburgerMenu as HamburgerIcon } from 'react-icons/gi';
import { MdSettings as SettingsIcon } from 'react-icons/md';
import { logout as _logout } from '../../redux/actions/userActions';
import rocketIcon from '../../assets/rocket.png';
import { classnames } from '../../utils';
import { GITHUB_PUBLIC_AUTH_URL, WRAPPED_URL } from '../../constants';
const propTypes = {
to: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func,
className: PropTypes.string,
};
const defaultProps = {
onClick: null,
className: null,
};
const StandardLink = ({ to, children, onClick, className }) => (
{children}
);
StandardLink.propTypes = propTypes;
StandardLink.defaultProps = defaultProps;
const MobileLink = ({ to, children, onClick, className }) => (
{children}
);
MobileLink.propTypes = propTypes;
MobileLink.defaultProps = defaultProps;
const Header = ({ mode }) => {
const [toggle, setToggle] = useState(false);
const userId = useSelector((state) => state.user.userId);
const isAuthenticated = userId && userId.length > 0;
const dispatch = useDispatch();
const logout = () => dispatch(_logout());
return (
{/* GitHub Trends Logo */}
{mode === 'trends' && (
GitHub Trends
)}
{mode === 'wrapped' && (
GitHub Wrapped
)}
{/* Pages: Wrapped, Dashboard, Demo */}
{mode === 'trends' && (
Wrapped
{isAuthenticated ? (
Dashboard
) : (
Demo
)}
)}
{/* Auth Pages: Sign Up, Log In, Log Out */}
{isAuthenticated ? (
<>
{mode === 'trends' && (
)}
Sign Out
>
) : (
<>
Login
Sign Up
>
)}
{/* Hamburger Menu */}
setToggle(!toggle)}
>
{/* Hamburger Dropdown */}
{mode === 'trends' && (
<>
setToggle(false)}>
Wrapped
{isAuthenticated ? (
setToggle(false)}>
Dashboard
) : (
setToggle(false)}>
Demo
)}
>
)}
{isAuthenticated ? (
<>
{mode === 'trends' && (
setToggle(false)}>
Settings
)}
{
setToggle(false);
logout();
}}
>
Sign Out
>
) : (
<>
Login
setToggle(false)}
className="block text-sm px-2 my-2 py-2 rounded-sm bg-blue-500 text-white"
>
Sign Up
>
)}
);
};
Header.propTypes = {
mode: PropTypes.string.isRequired,
};
export default Header;
================================================
FILE: frontend/src/pages/App/index.js
================================================
import AppTrends from './AppTrends';
import AppWrapped from './AppWrapped';
export { AppTrends, AppWrapped };
================================================
FILE: frontend/src/pages/Auth/SignUp.js
================================================
import React from 'react';
import { useSelector } from 'react-redux';
import { FaGithub as GithubIcon } from 'react-icons/fa';
import { Button } from '../../components';
import {
GITHUB_PUBLIC_AUTH_URL,
GITHUB_PRIVATE_AUTH_URL,
} from '../../constants';
import { classnames } from '../../utils';
import mockup from '../../assets/mockup.png';
const SignUpScreen = () => {
// eslint-disable-next-line no-unused-vars
const userId = useSelector((state) => state.user.userId);
return (
Sign up for
GitHub Trends
);
};
export default SignUpScreen;
================================================
FILE: frontend/src/pages/Auth/index.js
================================================
import SignUpScreen from './SignUp';
export { SignUpScreen };
================================================
FILE: frontend/src/pages/Demo/Demo.js
================================================
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Button, SvgInline } from '../../components';
import { getValidUser } from '../../api/wrapped';
import { BACKEND_URL } 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 () => {
const validUser = await getValidUser(userName);
if (validUser.includes('Valid user') || validUser === 'Repo not starred') {
setLoading(true);
setSelectedUserName(userName);
setLoading(false);
} else if (validUser === 'GitHub user not found') {
setError('GitHub user not found. Check your spelling and try again.');
}
};
const firstCardUrl =
selectedUserName.length > 0
? `${BACKEND_URL}/user/svg/${selectedUserName}/langs?demo=true`
: `${BACKEND_URL}/user/svg/demo?card=langs`;
const secondCardUrl =
selectedUserName.length > 0
? `${BACKEND_URL}/user/svg/${selectedUserName}/repos?demo=true`
: `${BACKEND_URL}/user/svg/demo?card=repos`;
return (
GitHub Trends Demo
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.
Enter your GitHub username to get started!
{
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();
}
}}
/>
Go
{error ? (
Error: {error}
) : (
)}
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!
Create an Account
{selectedUserName === ''
? 'Enter a Username'
: `Example Cards for ${selectedUserName}`}
);
};
export default DemoScreen;
================================================
FILE: frontend/src/pages/Demo/index.js
================================================
import DemoScreen from './Demo';
export default DemoScreen;
================================================
FILE: frontend/src/pages/Home/Home.js
================================================
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import BounceLoader from 'react-spinners/BounceLoader';
import { FaGithub as GithubIcon } from 'react-icons/fa';
import { ProgressBar } from '../../components';
import {
SelectCardStage,
CustomizeStage,
ThemeStage,
DisplayStage,
} from './stages';
import { setUserKey, authenticate } from '../../api';
import { login as _login } from '../../redux/actions/userActions';
import { PROD } from '../../constants';
const HomeScreen = () => {
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const userId = useSelector((state) => state.user.userId);
const privateAccess = useSelector((state) => state.user.privateAccess);
const isAuthenticated = userId && userId.length > 0;
const dispatch = useDispatch();
const login = (newUserId, userKey) => dispatch(_login(newUserId, userKey));
// for all stages
const [stage, setStage] = useState(0);
// for stage one
const [selectedCard, setSelectedCard] = useState('langs');
// for stage two
const defaultTimeRange = {
id: 3,
label: 'Past 1 Year',
disabled: false,
value: 'one_year',
};
const [selectedTimeRange, setSelectedTimeRange] = useState(defaultTimeRange);
const [usePercent, setUsePercent] = useState(false);
const [usePrivate, setUsePrivate] = useState(false);
const [groupOther, setGroupOther] = useState(false);
const [groupPrivate, setGroupPrivate] = useState(false);
const [useLocChanged, setUseLocChanged] = useState(false);
const [useCompact, setUseCompact] = useState(false);
const resetCustomization = () => {
setSelectedTimeRange(defaultTimeRange);
setUsePercent(false);
setUsePrivate(false);
setUseLocChanged(false);
setUseCompact(false);
};
useEffect(() => {
resetCustomization();
}, [selectedCard]);
const time = selectedTimeRange.value;
let fullSuffix = `${selectedCard}?time_range=${time}`;
if (usePercent) {
fullSuffix += '&use_percent=True';
}
if (usePrivate) {
fullSuffix += '&include_private=True';
}
if (usePrivate && groupOther && groupPrivate) {
fullSuffix += '&group=private';
} else if (groupOther) {
fullSuffix += '&group=other';
}
if (useLocChanged) {
fullSuffix += '&loc_metric=changed';
}
if (useCompact) {
fullSuffix += '&compact=True';
}
// for stage three
const [theme, setTheme] = useState('classic');
const themeSuffix = `${fullSuffix}&theme=${theme}`;
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 ? 'githubtrends.io' : 'localhost:3000';
const redirect = `${url.split(subStr)[0]}${subStr}/user`;
window.history.pushState({}, null, redirect);
setIsLoading(true);
const userKey = await setUserKey(newUrl[1]);
const newUserId = await authenticate(newUrl[1], tempPrivateAccess);
login(newUserId, userKey);
setIsLoading(false);
}
}
redirectCode();
}, []);
if (isLoading) {
return (
);
}
if (!isAuthenticated) {
return (
Please sign in to access this page
);
}
return (
{
[
'Select a Card',
'Modify Card Parameters',
'Choose a Theme',
'Display your Card',
][stage]
}
{
[
'You will be able to customize your card in future steps.',
'Change the date range, include private commits, and more!',
'Choose from one of our predefined themes (more coming soon!)',
'Display your card on GitHub, Twitter, or Linkedin',
][stage]
}
{stage === 0 && (
)}
{stage === 1 && (
)}
{stage === 2 && (
)}
{stage === 3 && (
)}
);
};
export default HomeScreen;
================================================
FILE: frontend/src/pages/Home/index.js
================================================
import HomeScreen from './Home';
export default HomeScreen;
================================================
FILE: frontend/src/pages/Home/stages/Customize.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { Image, DateRangeSection, CheckboxSection } from '../../../components';
const CustomizeStage = ({
selectedCard,
selectedTimeRange,
setSelectedTimeRange,
usePrivate,
setUsePrivate,
groupOther,
setGroupOther,
groupPrivate,
setGroupPrivate,
privateAccess,
useCompact,
setUseCompact,
usePercent,
setUsePercent,
useLocChanged,
setUseLocChanged,
fullSuffix,
}) => {
return (
{selectedCard === 'langs' && (
)}
{selectedCard === 'repos' && (
)}
{selectedCard === 'repos' && usePrivate && groupOther && (
)}
{selectedCard === 'langs' && (
)}
);
};
CustomizeStage.propTypes = {
selectedCard: PropTypes.string.isRequired,
selectedTimeRange: PropTypes.object.isRequired,
setSelectedTimeRange: PropTypes.func.isRequired,
usePrivate: PropTypes.bool.isRequired,
setUsePrivate: PropTypes.func.isRequired,
groupOther: PropTypes.bool.isRequired,
setGroupOther: PropTypes.func.isRequired,
groupPrivate: PropTypes.bool.isRequired,
setGroupPrivate: PropTypes.func.isRequired,
privateAccess: PropTypes.bool.isRequired,
useCompact: PropTypes.bool.isRequired,
setUseCompact: PropTypes.func.isRequired,
usePercent: PropTypes.bool.isRequired,
setUsePercent: PropTypes.func.isRequired,
useLocChanged: PropTypes.bool.isRequired,
setUseLocChanged: PropTypes.func.isRequired,
fullSuffix: PropTypes.string.isRequired,
};
export default CustomizeStage;
================================================
FILE: frontend/src/pages/Home/stages/Display.js
================================================
/* eslint-disable react/no-array-index-key */
import React from 'react';
import PropTypes from 'prop-types';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { saveSvgAsPng } from 'save-svg-as-png';
import { Card, Button } from '../../../components';
import { classnames } from '../../../utils';
const DisplayStage = ({ userId, themeSuffix }) => {
const card = themeSuffix.split('?')[0];
const downloadPNG = () => {
saveSvgAsPng(document.getElementById('svg-card'), `${userId}_${card}.png`, {
scale: 2,
encoderOptions: 1,
});
};
const copyUrl = () => {
navigator.clipboard.writeText(
`https://api.githubtrends.io/user/svg/${userId}/${themeSuffix}`,
);
toast.info('Copied to Clipboard!', {
position: 'bottom-right',
autoClose: 1000,
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
progress: undefined,
});
};
return (
Copy the image URL or download the PNG. Share on GitHub, Twitter,
LinkedIn, or anywhere else!
{[
{ title: 'Copy URL', active: true, onClick: copyUrl },
{ title: 'Download PNG', active: true, onClick: downloadPNG },
].map((item, index) => (
{item.title}
))}
);
};
DisplayStage.propTypes = {
userId: PropTypes.string.isRequired,
themeSuffix: PropTypes.string.isRequired,
};
export default DisplayStage;
================================================
FILE: frontend/src/pages/Home/stages/SelectCard.js
================================================
/* eslint-disable react/no-array-index-key */
import React from 'react';
import PropTypes from 'prop-types';
import { Card } from '../../../components';
const SelectCardStage = ({ selectedCard, setSelectedCard }) => {
return (
{[
{
title: 'Language Contributions',
description: 'See your overall language breakdown',
imageSrc: 'langs',
},
{
title: 'Repository Contributions',
description: 'See your most contributed repositories',
imageSrc: 'repos',
},
].map((card, index) => (
setSelectedCard(card.imageSrc)}
>
))}
);
};
SelectCardStage.propTypes = {
selectedCard: PropTypes.string.isRequired,
setSelectedCard: PropTypes.func.isRequired,
};
export default SelectCardStage;
================================================
FILE: frontend/src/pages/Home/stages/Theme.js
================================================
/* eslint-disable react/no-array-index-key */
import React from 'react';
import PropTypes from 'prop-types';
import { Card } from '../../../components';
const ThemeStage = ({ theme, setTheme, fullSuffix }) => {
return (
{[
{
title: 'Classic',
imageSrc: 'classic',
},
{
title: 'Dark',
imageSrc: 'dark',
},
{
title: 'Bright Lights',
imageSrc: 'bright_lights',
},
{
title: 'Rosettes',
imageSrc: 'rosettes',
},
{
title: 'Ferns',
imageSrc: 'ferns',
},
{
title: 'Synthwaves',
imageSrc: 'synthwaves',
},
].map((card, index) => (
setTheme(card.imageSrc)}
>
))}
);
};
ThemeStage.propTypes = {
theme: PropTypes.string.isRequired,
setTheme: PropTypes.func.isRequired,
fullSuffix: PropTypes.string.isRequired,
};
export default ThemeStage;
================================================
FILE: frontend/src/pages/Home/stages/index.js
================================================
import SelectCardStage from './SelectCard';
import CustomizeStage from './Customize';
import ThemeStage from './Theme';
import DisplayStage from './Display';
export { SelectCardStage, CustomizeStage, ThemeStage, DisplayStage };
================================================
FILE: frontend/src/pages/Landing/Landing.js
================================================
/* eslint-disable react/jsx-one-expression-per-line */
import React from 'react';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { FaGithub as GithubIcon, FaCheck as CheckIcon } 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);
const isAuthenticated = userId && userId.length > 0;
return (
Discover and share code contribution insights
GitHub Trends dives deep into the GitHub API to bring you insightful
metrics on your contributions, broken by repository and language.
{isAuthenticated ? 'Visit Dashboard' : 'Get Started'}
Star on
Display your GitHub stats
with embeddable cards
1. Comprehensive
GitHub Trends counts each individual commit, across all your
open-source contributions. We surface line of code metrics by
repository and languages.
2. Customizable
Using the online dashboard, easily modify the time range, include
private commits, and choose your display theme.
3. Shareable
Share your GitHub Trends cards as a PNG on Twitter, or as a dynamic
embeddable image on your GitHub profile or personal website.
{isAuthenticated ? 'Visit Dashboard' : 'Try the Demo'}
{!isAuthenticated && (
Get Started
)}
Reflect on your year
with GitHub Wrapped
1. Detailed
GitHub Wrapped provides a breakdown of your contributions by date,
by date, time, repository, and language. Over 20 stats are
displayed.
2. Visual
Understand your coding contributions like never before with an
interactive calendar, bar charts, pie charts, and more.
3. Public
Share your GitHub Wrapped link with your friends and colleagues and
take a look at their contributions too.{' '}
No account required , although rate limiting may
apply.
Example
Get your Wrapped
GitHub Trends
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.
{[
{
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
{item.header}
{item.text}
))}
{!isAuthenticated && (
Ready to get started?
Create an account today.
Try Demo
Sign Up
)}
);
}
export default LandingScreen;
================================================
FILE: frontend/src/pages/Landing/index.js
================================================
import LandingScreen from './Landing';
export default LandingScreen;
================================================
FILE: frontend/src/pages/Misc/NoMatch.js
================================================
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '../../components';
const NoMatchScreen = () => {
return (
404
Page not Found
Please check the URL in the address bar and try again.
Go to Home
);
};
export default NoMatchScreen;
================================================
FILE: frontend/src/pages/Misc/Redirect.js
================================================
// eslint-disable-next-line no-unused-vars
import React, { useEffect } from 'react';
import { BACKEND_URL } from '../../constants';
const RedirectScreen = () => {
useEffect(() => {
async function redirectCode() {
// Take any query parameters and pass to redirect
const url = window.location.href;
const hasCode = url.includes('user/redirect');
// If Github API returns the code parameter
if (hasCode) {
const newUrl = url.split('user/redirect');
window.location.replace(`${BACKEND_URL}/auth/redirect${newUrl[1]}`);
}
}
redirectCode();
}, []);
return null;
};
export default RedirectScreen;
================================================
FILE: frontend/src/pages/Misc/index.js
================================================
import NoMatchScreen from './NoMatch';
import RedirectScreen from './Redirect';
export { NoMatchScreen, RedirectScreen };
================================================
FILE: frontend/src/pages/Settings/Settings.js
================================================
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useSelector, useDispatch } from 'react-redux';
import { Button } from '../../components';
import { logout as _logout } from '../../redux/actions/userActions';
import { deleteAccount } from '../../api';
import { classnames } from '../../utils';
import { GITHUB_PRIVATE_AUTH_URL, CLIENT_ID } from '../../constants';
const SectionButton = ({ name, implemented, isSelected, setSelected }) => {
return (
setSelected('accountTier')}
>
{name}
);
};
SectionButton.propTypes = {
name: PropTypes.string.isRequired,
implemented: PropTypes.bool,
isSelected: PropTypes.bool.isRequired,
setSelected: PropTypes.func.isRequired,
};
SectionButton.defaultProps = {
implemented: true,
};
function useOutsideAlerter(ref, action) {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
action();
}
}
// Bind the event listener
document.addEventListener('mousedown', handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener('mousedown', handleClickOutside);
};
}, [ref]);
}
const SettingsScreen = () => {
const [selected, setSelected] = useState('accountTier');
const [deleteModal, setDeleteModal] = useState(false);
const openDeleteModal = () => {
setDeleteModal(true);
};
const closeDeleteModal = () => {
setDeleteModal(false);
};
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef, closeDeleteModal);
const userId = useSelector((state) => state.user.userId);
const isAuthenticated = userId && userId.length > 0;
const userKey = useSelector((state) => state.user.userKey);
const privateAccess = useSelector((state) => state.user.privateAccess);
const accountTier = privateAccess ? 'Private Workflow' : 'Public Workflow';
const dispatch = useDispatch();
const logout = () => dispatch(_logout());
console.log(isAuthenticated, userId, userKey, privateAccess, accountTier);
if (!isAuthenticated) {
return (
Please sign in to access this page
);
}
return (
Account Settings
setSelected('accountTier')}
/>
setSelected('personalization')}
/>
setSelected('deleteAccount')}
/>
{selected === 'accountTier' && (
Account Tier
Current Tier:
{accountTier}
{privateAccess ? (
You have given GitHub Trends (read and write) access to all
public and private code contributions. We use your GitHub
API access token to make requests on your behalf. All of our
code is open-source and visible on our
GitHub repository
) : (
You have given GitHub Trends read access to your public
repositories. Upgrading to the Private Workflow will allow
us to better represent your code contributions. We use your
GitHub API access token to make requests on your behalf. All
of our code is open-source and visible on our
GitHub repository
)}
{privateAccess ? (
Downgrade to Public Access
) : (
Upgrade to Private Access
)}
)}
{selected === 'personalization' && (
Personalization
Coming soon!
)}
{selected === 'deleteAccount' && (
Delete Account
Deleting your account is permanent and cannot be undone. If
you are sure you want to delete your account, click the button
below to remove your statistics from GitHub Trends. This will
redirect you to a GitHub screen where you can revoke your
access token.
Permenantly Delete Account
)}
{deleteModal && (
Delete Account
Are you sure you want to continue? This action cannot be
undone.
setDeleteModal(false)}
>
Cancel
{
const success = await deleteAccount(userId, userKey);
if (success) {
logout();
window.location = `https://github.com/settings/connections/applications/${CLIENT_ID}`;
}
}}
>
Delete Account
)}
);
};
export default SettingsScreen;
================================================
FILE: frontend/src/pages/Settings/index.js
================================================
import SettingsScreen from './Settings';
export default SettingsScreen;
================================================
FILE: frontend/src/pages/Wrapped/SelectUser.js
================================================
/* 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 (
Reflect on your year
with GitHub Wrapped
Powered by{' '}
GitHub Trends
{' '}
(not affiliated with GitHub)
Step 1 : Star the GitHub repository.{' '}
Step 2 : Enter your GitHub username!
{
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();
}
}}
/>
{isLoading ? (
) : (
'Go'
)}
{error ? (
Error: {error}
) : (
)}
See some examples
{[
{
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) => (
))}
GitHub Trends
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.
{[
{
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
{item.header}
{item.text}
))}
);
};
export default SelectUserScreen;
================================================
FILE: frontend/src/pages/Wrapped/Wrapped.js
================================================
/* 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 ;
}
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 (
{!downloadLoading && (
)}
{`${userId}'s`}
CURR_YEAR - i,
).map((x) => ({ value: x, label: x }))}
value={{ value: year, label: year }}
onChange={(e) => {
window.location.href = `/${userId}/${e.value}`;
}}
/>
GitHub Wrapped
Private Access:{' '}
{userId === currUserId && usePrivate ? 'True' : 'False'}
{!(userId === currUserId && usePrivate) && (
)}
{data?.incomplete && (
Incomplete Data. Please refresh later to finish loading.
)}
{
setHighlightDays(
Array.from(
{ length: endStreak - startStreak + 1 },
(_, i) => startStreak + i,
),
);
}}
onMouseOut={() => {
setHighlightDays([]);
}}
/>
setHighlightDays(
Array.from(
{ length: endGap - startGap + 1 },
(_, i) => startGap + i,
),
)
}
onMouseOut={() => setHighlightDays([])}
/>
`${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([])}
/>
{
setHighlightColors(singleHoverScale);
setHighlightDays([data?.numeric_data?.misc?.best_day_index]);
}}
onMouseOut={() => {
setHighlightColors(hoverScale);
setHighlightDays([]);
}}
/>
{downloadLoading && (
Create your own at www.githubwrapped.io
)}
{
setDownloadLoading(true);
setTimeout(() => {
downloadImage();
setDownloadLoading(false);
}, 10);
}}
>
{downloadLoading ? (
) : (
)}
);
};
export default WrappedScreen;
================================================
FILE: frontend/src/pages/Wrapped/index.js
================================================
import SelectUserScreen from './SelectUser';
import WrappedScreen from './Wrapped';
export { SelectUserScreen, WrappedScreen };
================================================
FILE: frontend/src/pages/Wrapped/sections/Loading.js
================================================
/* 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 (
{showLoadingErrorMessage ? (
Something went wrong. Please try again in a couple minutes or raise an
issue on{' '}
GitHub
. Thank you!
) : (
<>
{showLoadingMessage ? (
{[
'Loading your Data...',
'Crunching Numbers...',
'Analyzing Trends...',
'Drawing Figures...',
'Almost there!',
].map((text, i) => (
{text}
))}
) : (
)}
>
)}
);
};
export default LoadingScreen;
================================================
FILE: frontend/src/pages/Wrapped/sections/LoadingV2.js
================================================
/* 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 (
);
} else if (i === currMonth) {
return (
);
} else {
return (
);
}
};
return (
{currMonth < 12 ? (
<>
Querying the GitHub API by Months
{Array.from({ length: 3 }).map((_, i) => getTile(i))}
{Array.from({ length: 3 }).map((_, i) => getTile(i + 3))}
{Array.from({ length: 3 }).map((_, i) => getTile(i + 6))}
{Array.from({ length: 3 }).map((_, i) => getTile(i + 9))}
>
) : (
Loading your data is taking longer than expected. Try refreshing the
page, and if that fails, raise an issue on{' '}
GitHub
. Thank you for your patience!
)}
);
};
export default LoadingScreen;
================================================
FILE: frontend/src/pages/Wrapped/sections/index.js
================================================
import LoadingScreen from './Loading';
export { LoadingScreen };
================================================
FILE: frontend/src/pages/Wrapped/sections/loading.css
================================================
.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;
}
}
================================================
FILE: frontend/src/redux/actions/userActions.js
================================================
export const LOGIN = 'LOGIN';
export const LOGOUT = 'LOGOUT';
export const SET_PRIVATE_ACCESS = 'SET_PRIVATE_ACCESS';
export function login(userId, userKey) {
return { type: LOGIN, payload: { userId, userKey } };
}
export function logout() {
return { type: LOGOUT, payload: {} };
}
export function setPrivateAccess(privateAccess) {
return { type: SET_PRIVATE_ACCESS, payload: { privateAccess } };
}
================================================
FILE: frontend/src/redux/logger.js
================================================
// eslint-disable-next-line no-unused-vars
const logger = (store) => (next) => (action) => {
// console.group(action.type);
// console.info('dispatching', action);
const result = next(action);
// console.log('next state', store.getState());
console.groupEnd();
return result;
};
export default logger;
================================================
FILE: frontend/src/redux/reducers/index.js
================================================
import { combineReducers } from 'redux';
import user from './user';
export default combineReducers({ user });
================================================
FILE: frontend/src/redux/reducers/user.js
================================================
import * as types from '../actions/userActions';
const initialState = {
userId: JSON.parse(localStorage.getItem('userId')) || null,
userKey: JSON.parse(localStorage.getItem('userKey')) || null,
privateAccess: null,
};
// eslint-disable-next-line default-param-last
export default (state = initialState, action) => {
switch (action.type) {
case types.LOGIN:
localStorage.setItem('userId', JSON.stringify(action.payload.userId));
localStorage.setItem('userKey', JSON.stringify(action.payload.userKey));
return {
...state,
userId: action.payload.userId,
userKey: action.payload.userKey,
};
case types.LOGOUT:
localStorage.clear();
return {
userId: null,
userKey: null,
privateAccess: null,
};
case types.SET_PRIVATE_ACCESS:
return {
...state,
privateAccess: action.payload.privateAccess,
};
default:
return state;
}
};
================================================
FILE: frontend/src/redux/store.js
================================================
import { applyMiddleware, createStore, compose } from 'redux';
import loggerMiddleware from './logger';
import rootReducer from './reducers';
import { USE_LOGGER } from '../constants';
export default function configureStore(intialState) {
let middlewares = [];
if (USE_LOGGER) {
middlewares = [loggerMiddleware];
}
const middlewareEnhancer = applyMiddleware(...middlewares);
const enhancers = [middlewareEnhancer];
const composedEnhancers = compose(...enhancers);
const store = createStore(rootReducer, intialState, composedEnhancers);
return store;
}
================================================
FILE: frontend/src/utils.js
================================================
export function sleep(ms) {
// eslint-disable-next-line no-promise-executor-return
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function classnames(...args) {
return args.join(' ');
}
================================================
FILE: frontend/tailwind.config.js
================================================
/* eslint-disable global-require */
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
screens: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
'3xl': '1792px',
},
extend: {
fontFamily: {
typist: ['Playfair Display SC', 'Courier New'],
},
},
},
plugins: [require('daisyui')],
};