diff --git a/src/App.scss b/src/App.scss index f798278..3503a3f 100644 --- a/src/App.scss +++ b/src/App.scss @@ -70,7 +70,7 @@ section + section { margin-top: 6rem; @media all and (min-width: 1000px) { - margin-top: 10rem; + margin-top: 4rem; } } diff --git a/src/contributions/Contributions.tsx b/src/contributions/Contributions.tsx index 057baa1..198ce60 100644 --- a/src/contributions/Contributions.tsx +++ b/src/contributions/Contributions.tsx @@ -1,4 +1,12 @@ -import { Fragment } from "react"; +import { + Fragment, + useRef, + useState, + useMemo, + RefObject, + useEffect, + useCallback, +} from "react"; import { useInfiniteQuery } from "react-query"; import { gql, GraphQLClient } from "graphql-request"; import subYears from "date-fns/subYears"; @@ -13,6 +21,9 @@ import { subDays, } from "date-fns"; +import { ReactComponent as ArrowLeft } from "./icons/arrow-left.svg"; +import { ReactComponent as ArrowRight } from "./icons/arrow-right.svg"; + import "./contributions.scss"; const token = process.env.REACT_APP_GITHUB_SECRET; @@ -27,27 +38,102 @@ export function Contributions() { const { isLoading, isFetching, data, hasPreviousPage, fetchPreviousPage } = useContributionsCalendar(); let lastLabeledWeek: Date; + const chart = useRef(null); + const viewport = useRef(null); + const totalContributions = data?.pages.reduce(function sum(total, page) { + return ( + total + + page.viewer.contributionsCollection.contributionCalendar + .totalContributions + ); + }, 0); return (
-

Code Contributions

+

Code Contributions ({totalContributions})

{isLoading ? (

Loading...

) : ( -
-
- - Mon - - - Wed - - - Fri - -
-
-
+ <> + +
+
+ + Mon + + + Wed + + + Fri + +
+
+
+ {data!.pages.map((page) => { + const { startedAt, endedAt, contributionCalendar } = + page.viewer.contributionsCollection; + + return ( + + {contributionCalendar.weeks.map((week) => { + const days = week.contributionDays; + const isCurrentWeek = week.contributionDays.some( + (day) => isSameWeek(parseDate(day.date), today) + ); + const placeholderSpots = isCurrentWeek + ? [...new Array(7 - days.length)] + : []; + + return ( + + {days.map((day) => { + const parsed = parseDate(day.date); + const offset = parsed.getDay() % 7; + const count = day.contributionCount; + const s = count === 1 ? "" : "s"; + const date = relativeDate(parsed); + const on = + date === "Today" || date === "Yesterday" + ? "" + : "on"; + + return ( + + ); + })} + + ); + })} +
+
+
-
- + {hasPreviousPage && ( +
+ +
+ )} +
+ + )} +
+ ); +} - return ( - - {contributionCalendar.weeks.map((week) => { - const firstDay = parseDate(week.firstDay); - const showLabel = - !lastLabeledWeek || - !isSameMonth(lastLabeledWeek, firstDay); - let markup = showLabel ? ( - - - {format(new Date(week.firstDay), "MMM yyyy")} - - - ) : ( - - ); +function Controls({ chart, viewport }: ControlProps) { + const scroll = useRef(0); + const { width, height } = useMeasure(chart); + const viewportDimensions = useMeasure(viewport); + const scrollSize = Math.max(width, height); + const viewportSize = Math.max( + viewportDimensions.width, + viewportDimensions.height + ); - if (showLabel) { - lastLabeledWeek = firstDay; - } + const isNewerEnabled = useCallback(() => { + return scroll.current !== 0; + }, []); - return markup; - })} - - ); - })} - - {hasPreviousPage && ( -
- -
- )} - - )} - + const isOlderEnabled = useCallback(() => { + return scroll.current + viewportSize < scrollSize; + }, [viewportSize, scrollSize]); + + const [newerEnabled, setNewerEnabled] = useState(isNewerEnabled); + const [olderEnabled, setOlderEnabled] = useState(isOlderEnabled); + + function scrollTo(position: number) { + viewport.current?.scrollTo({ + top: position, + left: position, + behavior: "smooth", + }); + } + + useEffect( + function bindListeners() { + const element = viewport.current; + const onScroll = function (e: Event) { + const { scrollTop, scrollLeft } = e.target as HTMLElement; + + // Since we can only scroll in one direction at a time this works + // for horizontal and vertical scroll position + scroll.current = Math.max(scrollTop, scrollLeft); + + setNewerEnabled(isNewerEnabled); + setOlderEnabled(isOlderEnabled); + }; + + element?.addEventListener("scroll", onScroll); + + return function onUnmount() { + element?.removeEventListener("scroll", onScroll); + }; + }, + [chart, isOlderEnabled, isNewerEnabled, viewport] ); + + useEffect( + function onDimensionsChanged() { + setNewerEnabled(isNewerEnabled); + setOlderEnabled(isOlderEnabled); + }, + [viewportSize, scrollSize, isNewerEnabled, isOlderEnabled] + ); + + return ( +
+ + +
+ ); +} + +interface ControlProps { + chart: RefObject; + viewport: RefObject; } const client = new GraphQLClient("https://api.github.com/graphql"); @@ -189,7 +299,9 @@ function useContributionsCalendar( contributionsCollection(from: "${start}", to: "${stop}") { startedAt endedAt + hasActivityInThePast contributionCalendar { + totalContributions weeks { firstDay contributionDays { @@ -210,6 +322,14 @@ function useContributionsCalendar( { staleTime: Infinity, getPreviousPageParam: (_, pages) => { + const lastPage = [...pages].pop(); + if ( + lastPage?.viewer.contributionsCollection.hasActivityInThePast === + false + ) { + return; + } + const nextPage = pages.reduce( ( earliestPage: ContributionsCalendarQuery, @@ -289,6 +409,38 @@ function relativeDate(input: Date) { return format(input, "MMM dd, yyyy"); } +class Rect { + top = 0; + right = 0; + bottom = 0; + left = 0; + width = 0; + height = 0; +} + +function useMeasure( + node: RefObject +): Rect { + const [measured, measure] = useState(new Rect()); + const observer = useMemo(() => { + return new ResizeObserver(([observed]) => measure(observed.contentRect)); + }, []); + + useEffect( + function bind() { + if (!node.current) return; + + observer.observe(node.current); + return function onUnmount() { + observer.disconnect(); + }; + }, + [node, observer] + ); + + return measured; +} + interface ContributionDay { contributionCount: number; contributionLevel: @@ -310,7 +462,9 @@ interface ContributionsCalendarQuery { contributionsCollection: { startedAt: string; endedAt: string; + hasActivityInThePast: boolean; contributionCalendar: { + totalContributions: number; weeks: ContributionWeek[]; }; }; diff --git a/src/contributions/contributions.scss b/src/contributions/contributions.scss index 52ca19f..8965f64 100644 --- a/src/contributions/contributions.scss +++ b/src/contributions/contributions.scss @@ -267,4 +267,28 @@ $box_outline: 2px; } } } + + .controls { + } + + .scroll-button { + border: none; + background: transparent; + + &:hover { + background: transparent; + border: none; + } + + &:disabled { + svg { + fill: rgba(white, 0.5); + } + } + + svg { + width: 2rem; + height: 2rem; + } + } } diff --git a/src/contributions/icons/arrow-left.svg b/src/contributions/icons/arrow-left.svg new file mode 100644 index 0000000..debf0ff --- /dev/null +++ b/src/contributions/icons/arrow-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/contributions/icons/arrow-right.svg b/src/contributions/icons/arrow-right.svg new file mode 100644 index 0000000..acc4d67 --- /dev/null +++ b/src/contributions/icons/arrow-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/footer/footer.scss b/src/footer/footer.scss index 2365d2b..8a7da16 100644 --- a/src/footer/footer.scss +++ b/src/footer/footer.scss @@ -246,10 +246,15 @@ $img_size: 173px; .width-holder { font: inherit; font-size: 0.75rem; + max-height: 4rem; @media all and (min-width: 350px) { font-size: 1rem; } + + @media all and (min-width: 450px) { + font-size: 1.25rem; + } } .width-holder { diff --git a/src/footer/rad.png b/src/footer/rad.png new file mode 100644 index 0000000..fcaf7fc Binary files /dev/null and b/src/footer/rad.png differ diff --git a/src/stats/stats.scss b/src/stats/stats.scss index 9791dc8..3746c56 100644 --- a/src/stats/stats.scss +++ b/src/stats/stats.scss @@ -1,4 +1,5 @@ @import "../variables.scss"; +$black: darken($primary, 35); .stats { text-align: center; @@ -32,7 +33,7 @@ } dl { - color: black; + color: $black; margin: 2rem 0; padding: 2rem; display: inline-block; @@ -79,7 +80,7 @@ dt { font-size: 0.75rem; - font-weight: 300; + font-weight: 600; border-left-color: rgba(black, 0.25); padding-bottom: 1.5rem;