From 8d97d023740e438361e659c6e133418e33343178 Mon Sep 17 00:00:00 2001
From: Daniel Karbach <daniel.karbach@localhorst.tv>
Date: Wed, 6 Apr 2022 15:35:15 +0200
Subject: [PATCH] clickable results

---
 .../js/components/results/DetailDialog.js     | 95 +++++++++++++++++++
 resources/js/components/results/Item.js       | 55 ++++-------
 resources/js/helpers/Result.js                | 37 ++++++++
 resources/js/i18n/de.js                       |  8 ++
 resources/js/i18n/en.js                       |  8 ++
 resources/sass/results.scss                   |  9 ++
 6 files changed, 173 insertions(+), 39 deletions(-)
 create mode 100644 resources/js/components/results/DetailDialog.js

diff --git a/resources/js/components/results/DetailDialog.js b/resources/js/components/results/DetailDialog.js
new file mode 100644
index 0000000..368575b
--- /dev/null
+++ b/resources/js/components/results/DetailDialog.js
@@ -0,0 +1,95 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Col, Form, Modal, Row } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+
+import Box from '../users/Box';
+import { getTime } from '../../helpers/Result';
+import { findResult } from '../../helpers/Participant';
+import { maySeeResults } from '../../helpers/permissions';
+import { withUser } from '../../helpers/UserContext';
+import i18n from '../../i18n';
+
+const getPlacement = result =>
+	`${result.placement}. (${i18n.t('results.points', { count: result.score })})`;
+
+const DetailDialog = ({
+	onHide,
+	participant,
+	round,
+	show,
+	tournament,
+	user,
+}) => {
+	const result = findResult(participant, round);
+	const maySee = maySeeResults(user, tournament, round);
+	return <Modal className="result-dialog" onHide={onHide} show={show}>
+		<Modal.Header closeButton>
+			<Modal.Title>
+				{i18n.t('results.details')}
+			</Modal.Title>
+		</Modal.Header>
+		<Modal.Body>
+			<Row>
+				<Form.Group as={Col} sm={6}>
+					<Form.Label>{i18n.t('results.round')}</Form.Label>
+					<div>
+						#{round.number || '?'}
+						{' '}
+						{i18n.t('rounds.date', { date: new Date(round.created_at) })}
+					</div>
+				</Form.Group>
+				<Form.Group as={Col} sm={6}>
+					<Form.Label>{i18n.t('results.runner')}</Form.Label>
+					<div><Box user={participant.user} /></div>
+				</Form.Group>
+				<Form.Group as={Col} sm={6}>
+					<Form.Label>{i18n.t('results.result')}</Form.Label>
+					<div>
+						{maySee && result && result.has_finished
+							? getTime(result, maySee)
+							: i18n.t('results.pending')}
+					</div>
+				</Form.Group>
+				<Form.Group as={Col} sm={6}>
+					<Form.Label>{i18n.t('results.placement')}</Form.Label>
+					<div>
+						{maySee && result && result.placement
+							? getPlacement(result)
+							: i18n.t('results.pending')}
+					</div>
+				</Form.Group>
+				{maySee && result && result.comment ?
+					<Form.Group as={Col} sm={12}>
+						<Form.Label>{i18n.t('results.comment')}</Form.Label>
+						<div>{result.comment}</div>
+					</Form.Group>
+				: null}
+			</Row>
+		</Modal.Body>
+		<Modal.Footer>
+			<Button onClick={onHide} variant="secondary">
+				{i18n.t('button.close')}
+			</Button>
+		</Modal.Footer>
+	</Modal>;
+};
+
+DetailDialog.propTypes = {
+	onHide: PropTypes.func,
+	participant: PropTypes.shape({
+		user: PropTypes.shape({
+		}),
+	}),
+	round: PropTypes.shape({
+		created_at: PropTypes.string,
+		number: PropTypes.number,
+	}),
+	show: PropTypes.bool,
+	tournament: PropTypes.shape({
+	}),
+	user: PropTypes.shape({
+	}),
+};
+
+export default withTranslation()(withUser(DetailDialog));
diff --git a/resources/js/components/results/Item.js b/resources/js/components/results/Item.js
index 0d73105..a2ee8e3 100644
--- a/resources/js/components/results/Item.js
+++ b/resources/js/components/results/Item.js
@@ -1,46 +1,14 @@
 import PropTypes from 'prop-types';
-import React from 'react';
-import { withTranslation } from 'react-i18next';
+import React, { useState } from 'react';
+import { Button } from 'react-bootstrap';
 
-import Icon from '../common/Icon';
+import DetailDialog from './DetailDialog';
 import Box from '../users/Box';
-import { formatTime } from '../../helpers/Result';
+import { getIcon, getTime } from '../../helpers/Result';
 import { findResult } from '../../helpers/Participant';
 import { maySeeResults } from '../../helpers/permissions';
 import { withUser } from '../../helpers/UserContext';
 
-const getIcon = (result, maySee) => {
-	if (!result || !result.has_finished) {
-		return <Icon.PENDING className="text-muted" size="lg" />;
-	}
-	if (result.forfeit && maySee) {
-		return <Icon.FORFEIT className="text-danger" size="lg" />;
-	}
-	if (result.placement === 1 && maySee) {
-		return <Icon.FIRST_PLACE className="text-gold" size="lg" />;
-	}
-	if (result.placement === 2 && maySee) {
-		return <Icon.SECOND_PLACE className="text-silver" size="lg" />;
-	}
-	if (result.placement === 3 && maySee) {
-		return <Icon.THIRD_PLACE className="text-bronze" size="lg" />;
-	}
-	return <Icon.FINISHED className="text-success" size="lg" />;
-};
-
-const getTime = (result, maySee) => {
-	if (!result || !maySee) {
-		return null;
-	}
-	if (result.time) {
-		return formatTime(result);
-	}
-	if (result.forfeit) {
-		return 'DNF';
-	}
-	return '?';
-};
-
 const getClassName = result => {
 	const classNames = ['status'];
 	if (result && result.has_finished) {
@@ -60,19 +28,28 @@ const Item = ({
 	tournament,
 	user,
 }) => {
+	const [showDialog, setShowDialog] = useState(false);
 	const result = findResult(participant, round);
 	const maySee = maySeeResults(user, tournament, round);
 	return <div className="result">
 		<Box user={participant.user} />
-		<div
+		<Button
 			className={getClassName(result)}
+			onClick={() => setShowDialog(true)}
 			title={maySee && result && result.comment ? result.comment : null}
 		>
 			<span className="time">
 				{getTime(result, maySee)}
 			</span>
 			{getIcon(result, maySee)}
-		</div>
+		</Button>
+		<DetailDialog
+			onHide={() => setShowDialog(false)}
+			participant={participant}
+			round={round}
+			show={showDialog}
+			tournament={tournament}
+		/>
 	</div>;
 };
 
@@ -89,4 +66,4 @@ Item.propTypes = {
 	}),
 };
 
-export default withTranslation()(withUser(Item));
+export default withUser(Item);
diff --git a/resources/js/helpers/Result.js b/resources/js/helpers/Result.js
index 06ade09..205a42b 100644
--- a/resources/js/helpers/Result.js
+++ b/resources/js/helpers/Result.js
@@ -1,3 +1,6 @@
+import React from 'react';
+import Icon from '../components/common/Icon';
+
 export const formatTime = result => {
 	const hours = `${Math.floor(result.time / 60 / 60)}`;
 	let minutes = `${Math.floor((result.time / 60) % 60)}`;
@@ -11,6 +14,38 @@ export const formatTime = result => {
 	return `${hours}:${minutes}:${seconds}`;
 };
 
+export const getIcon = (result, maySee) => {
+	if (!result || !result.has_finished) {
+		return <Icon.PENDING className="text-muted" size="lg" />;
+	}
+	if (result.forfeit && maySee) {
+		return <Icon.FORFEIT className="text-danger" size="lg" />;
+	}
+	if (result.placement === 1 && maySee) {
+		return <Icon.FIRST_PLACE className="text-gold" size="lg" />;
+	}
+	if (result.placement === 2 && maySee) {
+		return <Icon.SECOND_PLACE className="text-silver" size="lg" />;
+	}
+	if (result.placement === 3 && maySee) {
+		return <Icon.THIRD_PLACE className="text-bronze" size="lg" />;
+	}
+	return <Icon.FINISHED className="text-success" size="lg" />;
+};
+
+export const getTime = (result, maySee) => {
+	if (!result || !maySee) {
+		return null;
+	}
+	if (result.time) {
+		return formatTime(result);
+	}
+	if (result.forfeit) {
+		return 'DNF';
+	}
+	return '?';
+};
+
 export const parseTime = str => {
 	if (!str) return null;
 	return `${str}`.split(/[-.: ]+/).reduce((acc,time) => (60 * acc) + +time, 0);
@@ -18,5 +53,7 @@ export const parseTime = str => {
 
 export default {
 	formatTime,
+	getIcon,
+	getTime,
 	parseTime,
 };
diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js
index 09d6db7..322d0f2 100644
--- a/resources/js/i18n/de.js
+++ b/resources/js/i18n/de.js
@@ -144,14 +144,22 @@ export default {
 		results: {
 			addComment: 'Kommentieren',
 			comment: 'Kommentar',
+			details: 'Details',
 			edit: 'Ergebnis ändern',
 			editComment: 'Kommentar ändern',
 			forfeit: 'Aufgegeben',
+			pending: 'Ausstehend',
+			placement: 'Platzierung',
+			points_one: '{{ count }} Punkt',
+			points_other: '{{ count }} Punkte',
 			report: 'Ergebnis eintragen',
 			reportError: 'Fehler beim Eintragen :(',
 			reportPreview: 'Wird als {{ time }} festgehalten',
 			reportSuccess: 'Festgehalten',
 			reportTime: 'Zeit',
+			result: 'Ergebnis',
+			round: 'Runde',
+			runner: 'Runner',
 			time: 'Zeit: {{ time }}',
 		},
 		rounds: {
diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js
index 72548f6..acd3c48 100644
--- a/resources/js/i18n/en.js
+++ b/resources/js/i18n/en.js
@@ -144,14 +144,22 @@ export default {
 		results: {
 			addComment: 'Comment',
 			comment: 'Comment',
+			details: 'Details',
 			edit: 'Change result',
 			editComment: 'Edit comment',
 			forfeit: 'Forfeit',
+			pending: 'Pending',
+			placement: 'Placement',
+			points_one: '{{ count }} point',
+			points_other: '{{ count }} points',
 			report: 'Report result',
 			reportError: 'Error saving :(',
 			reportPreview: 'Will be recorded as {{ time }}',
 			reportSuccess: 'Stored, thanks :)',
 			reportTime: 'Time',
+			result: 'Result',
+			round: 'Round',
+			runner: 'Runner',
 			time: 'Time: {{ time }}',
 		},
 		rounds: {
diff --git a/resources/sass/results.scss b/resources/sass/results.scss
index d78ebc5..3907e0e 100644
--- a/resources/sass/results.scss
+++ b/resources/sass/results.scss
@@ -8,18 +8,27 @@
 
 		.status {
 			display: flex;
+			position: relative;
 			align-items: center;
 			justify-content: space-between;
 			margin-top: 1ex;
 			padding: 0.5em;
+			width: 100%;
 			min-width: 15ex;
+			border: none;
 			border-radius: 1ex;
 			background: $dark;
 			color: $light;
 
+			box-shadow: none;
+			transition: top 0.15s ease-in-out;
 			&.has-comment {
 				box-shadow: 0 0.5ex 0 $info;
 			}
+			&:active {
+				box-shadow: none;
+				top: 0.5ex;
+			}
 
 			.time {
 				min-width: 9ex;
-- 
2.39.5