diff --git a/src/components/logbook/ChartsSection.tsx b/src/components/logbook/ChartsSection.tsx index 4e68b1c42..20dccbf80 100644 --- a/src/components/logbook/ChartsSection.tsx +++ b/src/components/logbook/ChartsSection.tsx @@ -1,14 +1,16 @@ import { TickType } from '../../js/types' import DifficultyPyramid from './DifficultyPyramid' import OverviewChart from './OverviewChart' - +import Stats from './Stats' export interface ChartsSectionProps { tickList: TickType[] } const ChartsSection: React.FC = ({ tickList }) => { + const sortedList = tickList.sort((a, b) => a.dateClimbed - b.dateClimbed) return (
- + +
) diff --git a/src/components/logbook/DifficultyPyramid.tsx b/src/components/logbook/DifficultyPyramid.tsx index fedb19abb..5ff14a170 100644 --- a/src/components/logbook/DifficultyPyramid.tsx +++ b/src/components/logbook/DifficultyPyramid.tsx @@ -57,23 +57,17 @@ const DifficultyPyramid: React.FC = ({ tickList }) => { orientation='bottom' dataKey='xBottom' tick={{ fontSize: '10' }} - tickFormatter={(value) => { - if (value == null) return '' - const yds = ydsScale?.getGrade(parseInt(value)) ?? '' - const vscale = vScale?.getGrade(parseInt(value)) ?? '' - return `${yds}/${vscale}` - }} + tickFormatter={tickFormatScoreToYdsVscale} /> { const actual = parseInt(value) - yOffset return `${actual > 0 ? actual : ''}` }} /> - + @@ -89,3 +83,10 @@ const getScoreUSAForRouteAndBoulder = (grade: string): number => { } return score } + +export const tickFormatScoreToYdsVscale = (value: string): string => { + if (value == null) return '' + const yds = ydsScale?.getGrade(parseInt(value)) ?? '' + const vscale = vScale?.getGrade(parseInt(value)) ?? '' + return `${yds}/${vscale}` +} diff --git a/src/components/logbook/OverviewChart.tsx b/src/components/logbook/OverviewChart.tsx index 5d7b3b4f0..fff354c38 100644 --- a/src/components/logbook/OverviewChart.tsx +++ b/src/components/logbook/OverviewChart.tsx @@ -7,7 +7,7 @@ import { lastDayOfMonth, format } from 'date-fns' import { linearRegression, linearRegressionLine, minSorted, maxSorted, medianSorted } from 'simple-statistics' import { TickType } from '../../js/types' -import { ydsScale, vScale } from './DifficultyPyramid' +import { ydsScale, vScale, tickFormatScoreToYdsVscale } from './DifficultyPyramid' export interface OverviewChartProps { tickList: TickType[] @@ -23,7 +23,7 @@ const OverviewChart: React.FC = ({ tickList }) => { const xyRegressionData: number[][] = [] - const chartData: ChartDataPayloadProps[] = Object.entries(agg).reverse().map(value => { + const chartData: ChartDataPayloadProps[] = Object.entries(agg).map(value => { const x = parseInt(value[0]) const gradeScores = value[1].reduce((acc, curr) => { let score = ydsScale?.getScore(curr.grade)?.[0] as number ?? -1 @@ -73,12 +73,11 @@ const OverviewChart: React.FC = ({ tickList }) => { { - return parseInt(value) <= 0 ? ' ' : value - }} + yAxisId='score' stroke='rgb(15 23 42)' tick={{ fontSize: '10' }} tickFormatter={tickFormatScoreToYdsVscale} /> @@ -175,9 +174,9 @@ const CustomTooltip: React.FC = ({ active, payload, label }) => { return (
Total climbs: {payload[4].value}
-
Median: {payload[0].value}
-
Low: {payload[1].value}
-
High: {payload[2].value}
+
Median: {tickFormatScoreToYdsVscale(payload[0].value)}
+
Low: {tickFormatScoreToYdsVscale(payload[1].value)}
+
High: {tickFormatScoreToYdsVscale(payload[2].value)}
) } diff --git a/src/components/logbook/Stats.tsx b/src/components/logbook/Stats.tsx new file mode 100644 index 000000000..d9a1bf2d6 --- /dev/null +++ b/src/components/logbook/Stats.tsx @@ -0,0 +1,70 @@ +import { groupBy } from 'underscore' +import { formatDistanceToNowStrict, format, endOfDay, differenceInCalendarDays } from 'date-fns' +import { TickType } from '../../js/types' + +const Stats: React.FC<{ tickList: TickType[]}> = ({ tickList }) => { + const sortedList = tickList + const total = tickList.length + const totalTime = formatDistanceToNowStrict(sortedList[0].dateClimbed) + + const dayMap = groupBy(sortedList, getEndOfDay) + const climbingDays = Object.keys(dayMap).length + + const longestStreak = calculateLongestStreak(sortedList) + return ( +
+ +
+
Total
+
{total}
+
sends
+ +
+ +
+
Time
+
{totalTime}
+
since {format(sortedList[0].dateClimbed, 'MMMM dd, yyyy')}
+
+ +
+
Climbing days
+
{climbingDays}
+
 
+
+ +
+
Longest streak
+
{longestStreak}
+
consecutive days
+
+
+ ) +} + +export default Stats + +const getEndOfDay = (entry: TickType): number => endOfDay(entry.dateClimbed).getTime() + +export const calculateLongestStreak = (sortedList: TickType[]): number | null => { + const streakSet = new Set() + let longestStreak: Date[] = [] + for (let i = 0; i < sortedList.length; i++) { + const today = new Date(sortedList[i].dateClimbed) + if (i === sortedList.length - 1) { + longestStreak.push(today) + break + } + const nextDay = sortedList[i + 1].dateClimbed + if (differenceInCalendarDays(nextDay, today) === 1) { + longestStreak.push(today) + } else { + if (longestStreak.length > 0) { + streakSet.add(longestStreak.length + 1) + } + longestStreak = [] + } + } + const list = Array.from(streakSet.keys()).sort((a, b) => b - a) + return list.length === 0 ? null : list[0] +} diff --git a/src/pages/u2/[...slug].tsx b/src/pages/u2/[...slug].tsx index dbb359957..4cab2d7cd 100644 --- a/src/pages/u2/[...slug].tsx +++ b/src/pages/u2/[...slug].tsx @@ -1,5 +1,5 @@ import React from 'react' - +import { useRouter } from 'next/router' import { NextPage, GetStaticProps } from 'next' import dynamic from 'next/dynamic' import Link from 'next/link' @@ -22,26 +22,33 @@ interface TicksIndexPageProps { * - Incrementally adopt nested layout https://nextjs.org/blog/layouts-rfc */ const Index: NextPage = ({ username, ticks }) => { + const { isFallback } = useRouter() + return ( - + {isFallback + ?
Loading...
+ : ( + <> + -
-

{username}

- +
+

{username}

+ -

Log book

-
- {ticks?.map(Tick)} - {ticks?.length === 0 &&
No ticks
} -
-
+

Log book

+
+ {ticks?.map(Tick)} + {ticks?.length === 0 &&
No ticks
} +
+
+ )}
) }