From 3adea161fd38bb256e4fae6197affda31bb5a9af Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Mon, 22 Nov 2021 11:44:00 +0000 Subject: [PATCH] Timeline View --- .../_playtype/folders/SpecificFolderPage.tsx | 227 +++++++++++++++++- 1 file changed, 224 insertions(+), 3 deletions(-) diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx index 5cc282f0e..d21180d75 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx @@ -11,28 +11,36 @@ import Muted from "components/util/Muted"; import useApiQuery from "components/util/query/useApiQuery"; import SelectButton from "components/util/SelectButton"; import { useFormik } from "formik"; -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { Col, Form, Row } from "react-bootstrap"; import { Link, useParams } from "react-router-dom"; import { + ChartDocument, ChartTierlistInfo, + FormatDifficulty, FormatDifficultyShort, Game, GetGamePTConfig, GPTTierlists, IDStrings, + integer, PublicUserDocument, + ScoreDocument, + SongDocument, } from "tachi-common"; import { UGPTFolderReturns } from "types/api-returns"; +import { JustChildren } from "types/react"; import { FolderDataset } from "types/tables"; import { Playtype } from "types/tachi"; import { DEFAULT_BAR_PROPS } from "util/charts"; import { ChangeOpacity } from "util/color-opacity"; +import { ONE_DAY } from "util/constants/time"; import { CreateChartIDMap, CreateChartLink, CreateSongMap } from "util/data"; import { ComposeExpFn, ComposeInverseExpFn, IsNullish, NO_OP } from "util/misc"; import { GetGradeChartExpScale } from "util/scales"; import { NumericSOV, StrSOV } from "util/sorts"; import { GetScaleAchievedFn } from "util/tierlist"; +import { FormatDate, FormatDuration, FormatTime } from "util/time"; interface Props { reqUser: PublicUserDocument; @@ -47,7 +55,7 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { `/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}` ); - const [mode, setMode] = useState<"normal" | "ladder">("normal"); + const [mode, setMode] = useState<"normal" | "ladder" | "timeline">("normal"); const gptConfig = GetGamePTConfig(game, playtype); @@ -122,6 +130,10 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { Tierlist View )} + + + Timeline View +
@@ -136,7 +148,7 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { reqUser={reqUser} indexCol={false} /> - ) : ( + ) : mode === "ladder" ? ( + ) : ( + )}
); } +function TimelineView({ game, playtype, reqUser, folderID }: Props & { folderID: string }) { + const gptConfig = GetGamePTConfig(game, playtype); + const [type, setType] = useState<"lamp" | "grade">(gptConfig.scoreBucket); + const [value, setValue] = useState( + type === "grade" ? gptConfig.grades.length - 1 : gptConfig.lamps.length - 1 + ); + + useEffect(() => { + setValue(type === "grade" ? gptConfig.grades.length - 1 : gptConfig.lamps.length - 1); + }, [type]); + + return ( + <> + +
+
+
+ The timeline view shows the order in which you achieved something in a + folder! You can choose the criteria up here. +
+ +
+
+ setType(e.target.value as "grade" | "lamp")} + > + + + +
+
+ setValue(Number(e.target.value))} + > + {type === "grade" + ? gptConfig.grades.map((e, i) => ( + + )) + : gptConfig.lamps.map((e, i) => ( + + ))} + +
+
+
+ + + + ); +} + +function TimelineMain({ + reqUser, + game, + playtype, + folderID, + type, + value, +}: Props & { + folderID: string; + type: "grade" | "lamp"; + value: integer; +}) { + const { data, isLoading, error } = useApiQuery<{ + scores: ScoreDocument[]; + songs: SongDocument[]; + charts: ChartDocument[]; + }>( + `/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}/timeline?criteriaValue=${value}&criteriaType=${type}` + ); + + if (error) { + return ; + } + + if (isLoading || !data) { + return ; + } + + const scoreDataset = []; + + const songMap = CreateSongMap(data.songs); + const chartMap = CreateChartIDMap(data.charts); + + for (const score of data.scores) { + scoreDataset.push({ + ...score, + __related: { + song: songMap.get(score.songID)!, + chart: chartMap.get(score.chartID)!, + }, + }); + } + + scoreDataset.sort(NumericSOV(x => x.timeAchieved ?? Infinity)); + + const elements = []; + + let lastDay = 0; + let index = 1; + let hasHitNulls = false; + + for (const scoreData of scoreDataset) { + if (scoreData.timeAchieved !== null) { + // Insane hack to floor a date to the beginning of that + // day. + const dayNum = new Date(scoreData.timeAchieved).setHours(0, 0, 0, 0); + + if (!lastDay || lastDay !== dayNum) { + lastDay = dayNum; + elements.push( + {FormatDate(scoreData.timeAchieved)} + ); + } + } else if (!hasHitNulls) { + elements.push(Unknown Time); + hasHitNulls = true; + } + + elements.push( + + ); + index++; + } + + return ( + <> +
+
+ {elements} +
+ +
+

Total Progress

+

+ {data.scores.length} + + /{data.charts.length} + +

+
+ + ); +} + +function TimelineDivider({ children }: { children: string }) { + return ( +
+

{children}

+
+ ); +} + +function TimelineElement({ + scoreData, + index, +}: { + index: integer; + scoreData: ScoreDocument & { + __related: { + song: SongDocument; + chart: ChartDocument; + }; + }; +}) { + return ( +
+ +
+ + #{index}:{" "} + + {scoreData.__related.song.title}{" "} + {FormatDifficulty(scoreData.__related.chart, scoreData.game)} + + {Date.now() - scoreData.timeAdded < ONE_DAY && ( + + NEW! + + )} + + + {scoreData.timeAchieved === null + ? "Unknown Time" + : FormatTime(scoreData.timeAchieved)} + +
+
+ ); +} + type InfoProps = Props & { folderDataset: FolderDataset; data: UGPTFolderReturns;