mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-05 05:18:12 +03:00
Merge pull request #632 from TNG-dev/zkldi/session-raise-comments
This commit is contained in:
@@ -264,7 +264,7 @@ function TopShowcase({
|
||||
if (user?.id === bestPlay.userID) {
|
||||
return (
|
||||
<Col xs={12}>
|
||||
<PlayCard name="Best Play" pb={bestPlay} user={bestUser} />
|
||||
<PlayCard name="Best PB" pb={bestPlay} user={bestUser} />
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
@@ -273,10 +273,10 @@ function TopShowcase({
|
||||
return (
|
||||
<>
|
||||
<Col xs={12} lg={6}>
|
||||
<PlayCard name="Best Play" pb={bestPlay} user={bestUser} />
|
||||
<PlayCard name="Best PB" pb={bestPlay} user={bestUser} />
|
||||
</Col>
|
||||
<Col xs={12} lg={6}>
|
||||
<Card header="Your Score" className="text-center">
|
||||
<Card header="Your PB" className="text-center">
|
||||
<Muted>You've not played this chart.</Muted>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -326,7 +326,7 @@ function PlayCard({
|
||||
<span className="text-muted">/{pb.rankingData.outOf}</span>
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<MiniTable headers={["Score Info"]}>
|
||||
<MiniTable headers={["PB Info"]}>
|
||||
<tr>
|
||||
<ScoreCell score={pb} />
|
||||
</tr>
|
||||
|
||||
@@ -18,7 +18,7 @@ import SelectButton from "components/util/SelectButton";
|
||||
import { useProfileRatingAlg } from "components/util/useScoreRatingAlg";
|
||||
import { DateTime } from "luxon";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Badge } from "react-bootstrap";
|
||||
import { Badge, Row } from "react-bootstrap";
|
||||
import {
|
||||
FormatGame,
|
||||
GetGameConfig,
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { SessionReturns, UGPTHistory } from "types/api-returns";
|
||||
import { GamePT } from "types/react";
|
||||
import { ScoreDataset } from "types/tables";
|
||||
import SessionRaiseBreakdown from "components/sessions/SessionRaiseBreakdown";
|
||||
|
||||
export default function OverviewPage({
|
||||
reqUser,
|
||||
@@ -84,81 +85,76 @@ function LastSession({ reqUser, game, playtype }: { reqUser: PublicUserDocument
|
||||
data && (
|
||||
<Card className="mt-4" header="Most Recent Session">
|
||||
<div className="row d-flex justify-content-center">
|
||||
{
|
||||
<>
|
||||
<div className="col-12 text-center">
|
||||
<h4 className="display-4">{data.session.name}</h4>
|
||||
<span className="text-muted">{data.session.desc}</span>
|
||||
<Divider className="mt-4 mb-8" />
|
||||
</div>
|
||||
<div className="col-12 text-center">
|
||||
<h4 className="display-4">{data.session.name}</h4>
|
||||
<span className="text-muted">{data.session.desc}</span>
|
||||
<Divider className="mt-4 mb-8" />
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-lg-3 text-center">
|
||||
<MiniTable
|
||||
className="mt-4"
|
||||
headers={["Information"]}
|
||||
colSpan={2}
|
||||
>
|
||||
<tr>
|
||||
<td>Started</td>
|
||||
<td>{FormatTime(data.session.timeStarted)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{/* If session ended less than 2 hours ago, it can still be appended to. */}
|
||||
{/* Kinda - time is a bit nonlinear wrt. importing scores, as scores don't have to have */}
|
||||
{/* happened at the same time they were imported. */}
|
||||
{/* that doesn't matter though - this is just a UI thing */}
|
||||
<td>
|
||||
{Date.now() - data.session.timeEnded <
|
||||
ONE_HOUR * 2
|
||||
? "Last Score"
|
||||
: "Ended At"}
|
||||
</td>
|
||||
<td>
|
||||
{FormatTime(data.session.timeEnded)}
|
||||
{/* if the last score was less than 10 mins ago, this session is probably still being populated */}
|
||||
{Date.now() - data.session.timeEnded <
|
||||
ONE_MINUTE * 10 && (
|
||||
<>
|
||||
<br />
|
||||
<Badge
|
||||
variant="primary"
|
||||
className="mt-2"
|
||||
>
|
||||
Ongoing!
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Duration</td>
|
||||
<td>
|
||||
{FormatDuration(
|
||||
data.session.timeEnded -
|
||||
data.session.timeStarted
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scores</td>
|
||||
<td>{data.session.scoreInfo.length}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PBs</td>
|
||||
<td>{GetPBs(data.session.scoreInfo).length}</td>
|
||||
</tr>
|
||||
</MiniTable>
|
||||
</div>
|
||||
<div className="col-12 col-lg-9">
|
||||
<RecentSessionScoreInfo
|
||||
{...data}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
reqUser={reqUser}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
<div className="col-12 col-lg-3 text-center">
|
||||
<MiniTable className="mt-4" headers={["Information"]} colSpan={2}>
|
||||
<tr>
|
||||
<td>Started</td>
|
||||
<td>{FormatTime(data.session.timeStarted)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{/* If session ended less than 2 hours ago, it can still be appended to. */}
|
||||
{/* Kinda - time is a bit nonlinear wrt. importing scores, as scores don't have to have */}
|
||||
{/* happened at the same time they were imported. */}
|
||||
{/* that doesn't matter though - this is just a UI thing */}
|
||||
<td>
|
||||
{Date.now() - data.session.timeEnded < ONE_HOUR * 2
|
||||
? "Last Score"
|
||||
: "Ended At"}
|
||||
</td>
|
||||
<td>
|
||||
{FormatTime(data.session.timeEnded)}
|
||||
{/* if the last score was less than 10 mins ago, this session is probably still being populated */}
|
||||
{Date.now() - data.session.timeEnded <
|
||||
ONE_MINUTE * 10 && (
|
||||
<>
|
||||
<br />
|
||||
<Badge variant="primary" className="mt-2">
|
||||
Ongoing!
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Duration</td>
|
||||
<td>
|
||||
{FormatDuration(
|
||||
data.session.timeEnded - data.session.timeStarted
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scores</td>
|
||||
<td>{data.session.scoreInfo.length}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PBs</td>
|
||||
<td>{GetPBs(data.session.scoreInfo).length}</td>
|
||||
</tr>
|
||||
</MiniTable>
|
||||
</div>
|
||||
<div className="col-12 col-lg-9">
|
||||
<Row>
|
||||
<SessionRaiseBreakdown
|
||||
sessionData={data.sessionData}
|
||||
setScores={() => void 0}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<Divider />
|
||||
<RecentSessionScoreInfo
|
||||
session={data.session}
|
||||
sessionData={data.sessionData}
|
||||
reqUser={reqUser}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
@@ -170,14 +166,14 @@ function LastSession({ reqUser, game, playtype }: { reqUser: PublicUserDocument
|
||||
function RecentSessionScoreInfo({
|
||||
session,
|
||||
sessionData,
|
||||
game,
|
||||
playtype,
|
||||
reqUser,
|
||||
}: {
|
||||
session: SessionDocument;
|
||||
sessionData: SessionReturns;
|
||||
reqUser: PublicUserDocument;
|
||||
} & GamePT) {
|
||||
}) {
|
||||
const { game, playtype } = session;
|
||||
|
||||
const highlightedScores = sessionData.scores.filter((e) => e.highlight);
|
||||
|
||||
const [mode, setMode] = useState<"highlight" | "best" | "recent">(
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function SessionOverview({
|
||||
<Col xs={12}>
|
||||
<Divider />
|
||||
|
||||
<Card header="Breakdown">
|
||||
<Card header="Raise Breakdown">
|
||||
<Row>
|
||||
<SessionRaiseBreakdown sessionData={sessionData} setScores={setScores} />
|
||||
</Row>
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
TableDocument,
|
||||
} from "tachi-common";
|
||||
import { SessionReturns } from "types/api-returns";
|
||||
import { ModifyScore } from "components/tables/dropdowns/components/ScoreEditButtons";
|
||||
import { CommentModal, ModifyScore } from "components/tables/dropdowns/components/ScoreEditButtons";
|
||||
import { SetState } from "types/react";
|
||||
import { ScoreDataset } from "types/tables";
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function SessionRaiseBreakdown({
|
||||
<>
|
||||
<div className="col-12">
|
||||
<div className="row">
|
||||
<div className="col-6 col-lg-6 offset-lg-3">
|
||||
<div className="col-12 col-lg-6 offset-lg-3">
|
||||
<div className="d-flex justify-content-center">
|
||||
<div className="btn-group">
|
||||
<SelectButton value={view} setValue={setView} id="lamps">
|
||||
@@ -138,7 +138,7 @@ export default function SessionRaiseBreakdown({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-6 col-lg-3">
|
||||
<div className="col-lg-3 d-none d-lg-block">
|
||||
<select
|
||||
className="form-control ml-auto"
|
||||
value={filter}
|
||||
@@ -357,7 +357,7 @@ function ElementStatTable({
|
||||
fullSize?: boolean;
|
||||
}) {
|
||||
function makeModifyScoreFn(score: ScoreDocument) {
|
||||
return ({ highlight, comment }: { highlight?: boolean; comment?: string }) => {
|
||||
return ({ highlight, comment }: { highlight?: boolean; comment?: string | null }) => {
|
||||
const scoreID = score.scoreID;
|
||||
|
||||
ModifyScore(scoreID, { highlight, comment }).then((r) => {
|
||||
@@ -464,13 +464,14 @@ function BreakdownChartContents({
|
||||
songMap: Map<integer, SongDocument>;
|
||||
chartMap: Map<string, ChartDocument>;
|
||||
gptConfig: GamePTConfig;
|
||||
modifyScore: ({ highlight, comment }: { highlight?: boolean; comment?: string }) => void;
|
||||
modifyScore: ({ highlight, comment }: { highlight?: boolean; comment?: string | null }) => void;
|
||||
type: "lamp" | "grade";
|
||||
}) {
|
||||
const chart = chartMap.get(score.chartID)!;
|
||||
const song = songMap.get(score.songID)!;
|
||||
|
||||
const [highlight, setHighlight] = useState(score.highlight);
|
||||
const [comment, setComment] = useState(score.comment);
|
||||
const [firstRun, setFirstRun] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -479,8 +480,8 @@ function BreakdownChartContents({
|
||||
return;
|
||||
}
|
||||
|
||||
modifyScore({ highlight });
|
||||
}, [highlight]);
|
||||
modifyScore({ highlight, comment });
|
||||
}, [highlight, comment]);
|
||||
|
||||
if (!chart || !song) {
|
||||
console.error(`No chart for ${score.chartID}/${score.songID}???`);
|
||||
@@ -531,22 +532,55 @@ function BreakdownChartContents({
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleCell noArtist chart={chart} game={game} song={song} />
|
||||
<PseudHighlight highlight={highlight} setHighlight={setHighlight} />
|
||||
<TitleCell noArtist chart={chart} game={game} song={song} comment={comment} />
|
||||
<CommentHighlightManager
|
||||
highlight={highlight}
|
||||
setHighlight={setHighlight}
|
||||
comment={comment}
|
||||
setComment={setComment}
|
||||
/>
|
||||
<DifficultyCell alwaysShort chart={chart} game={game} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PseudHighlight({
|
||||
/**
|
||||
* It manages the comment and highlight stuff.
|
||||
*
|
||||
* I don't know what else to call this function.
|
||||
*/
|
||||
function CommentHighlightManager({
|
||||
highlight,
|
||||
setHighlight,
|
||||
comment,
|
||||
setComment,
|
||||
}: {
|
||||
highlight: boolean;
|
||||
setHighlight: (hl: boolean) => void;
|
||||
comment: string | null;
|
||||
setComment: (cm: string | null) => void;
|
||||
}) {
|
||||
const [showCommentModal, setShowCommentModal] = useState(false);
|
||||
|
||||
return (
|
||||
<td style={{ verticalAlign: "center" }}>
|
||||
<CommentModal
|
||||
show={showCommentModal}
|
||||
setShow={setShowCommentModal}
|
||||
initialComment={comment}
|
||||
onUpdate={(comment) => {
|
||||
setComment(comment);
|
||||
setShowCommentModal(false);
|
||||
}}
|
||||
/>
|
||||
<span className="breakdown-hover-highlight-button">
|
||||
<Icon
|
||||
onClick={() => setShowCommentModal(true)}
|
||||
type="comment"
|
||||
regular
|
||||
style={{ paddingTop: "0.1rem", paddingRight: "0.33rem" }}
|
||||
/>
|
||||
</span>
|
||||
{highlight ? (
|
||||
<Icon
|
||||
onClick={() => setHighlight(false)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { APIFetchV1 } from "util/api";
|
||||
import QuickTooltip from "components/layout/misc/QuickTooltip";
|
||||
import Icon from "components/util/Icon";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import React, { useContext, useState } from "react";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { Game, ScoreDocument } from "tachi-common";
|
||||
import { Button, Modal, Form } from "react-bootstrap";
|
||||
import { SetState } from "types/react";
|
||||
@@ -101,10 +101,16 @@ export default function ScoreEditButtons({
|
||||
<CommentModal
|
||||
show={show}
|
||||
setShow={setShow}
|
||||
comment={comment}
|
||||
setComment={setComment}
|
||||
scoreID={score.scoreID}
|
||||
score={score}
|
||||
initialComment={comment}
|
||||
onUpdate={(comment) => {
|
||||
ModifyScore(score.scoreID, { comment }).then((r) => {
|
||||
if (r) {
|
||||
setComment(comment);
|
||||
score.comment = comment;
|
||||
setShow(false);
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -130,22 +136,26 @@ export async function ModifyScore(
|
||||
return res.success;
|
||||
}
|
||||
|
||||
function CommentModal({
|
||||
export function CommentModal({
|
||||
show,
|
||||
setShow,
|
||||
comment,
|
||||
setComment,
|
||||
scoreID,
|
||||
score,
|
||||
initialComment,
|
||||
onUpdate,
|
||||
}: {
|
||||
show: boolean;
|
||||
setShow: SetState<boolean>;
|
||||
comment: string | null;
|
||||
setComment: SetState<string | null>;
|
||||
scoreID: string;
|
||||
score: ScoreDocument;
|
||||
initialComment: string | null;
|
||||
onUpdate: (newComment: string | null) => void;
|
||||
}) {
|
||||
const [innerComment, setInnerComment] = useState(comment ?? "");
|
||||
const [innerComment, setInnerComment] = useState(initialComment ?? "");
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (show && ref.current) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
return (
|
||||
<Modal show={show} onHide={() => setShow(false)}>
|
||||
@@ -157,24 +167,25 @@ function CommentModal({
|
||||
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
ModifyScore(scoreID, { comment: innerComment }).then((r) => {
|
||||
if (r) {
|
||||
setComment(innerComment);
|
||||
score.comment = innerComment;
|
||||
setShow(false);
|
||||
}
|
||||
});
|
||||
if (innerComment === "") {
|
||||
onUpdate(null);
|
||||
} else {
|
||||
onUpdate(innerComment);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form.Group>
|
||||
<div className="input-group">
|
||||
<input
|
||||
ref={ref}
|
||||
autoFocus
|
||||
className="form-control form-control-lg"
|
||||
type="text"
|
||||
placeholder={comment ?? "This score was great!"}
|
||||
placeholder={initialComment ?? "This score was great!"}
|
||||
value={innerComment}
|
||||
onChange={(e) => setInnerComment(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="input-group-append">
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
@@ -183,19 +194,15 @@ function CommentModal({
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
{comment !== null && (
|
||||
{initialComment !== null && (
|
||||
<QuickTooltip tooltipContent="Remove your comment on this score.">
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
onClick={() =>
|
||||
ModifyScore(scoreID, { comment: null }).then((r) => {
|
||||
if (r) {
|
||||
setComment(null);
|
||||
score.comment = null;
|
||||
setShow(false);
|
||||
}
|
||||
})
|
||||
}
|
||||
onClick={() => {
|
||||
onUpdate(null);
|
||||
|
||||
setInnerComment("");
|
||||
}}
|
||||
>
|
||||
<Icon noPad type="trash" />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user