]> git.localhorst.tv Git - alttp.git/commitdiff
add forfeit result
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Tue, 15 Mar 2022 16:07:35 +0000 (17:07 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Tue, 15 Mar 2022 16:07:35 +0000 (17:07 +0100)
17 files changed:
app/Http/Controllers/ResultController.php
app/Models/Protocol.php
app/Models/Result.php
database/migrations/2022_03_15_081701_add_forfeit_result.php [new file with mode: 0644]
package.json
resources/js/bootstrap.js
resources/js/components/common/Icon.js
resources/js/components/common/LargeCheck.js [new file with mode: 0644]
resources/js/components/results/Item.js
resources/js/components/results/ReportForm.js
resources/js/helpers/Participant.js
resources/js/helpers/Result.js
resources/js/helpers/laravelErrorsToFormik.js [new file with mode: 0644]
resources/js/i18n/de.js
resources/js/schema/yup.js
resources/sass/app.scss
resources/sass/form.scss [new file with mode: 0644]

index 37ebf3b4acc51b995bf47b1f0f4d3686814f29da..b23c907f680b28edcc5dbb40efc1cbcd44c8933b 100644 (file)
@@ -14,10 +14,12 @@ class ResultController extends Controller
 
        public function create(Request $request) {
                $validatedData = $request->validate([
+                       'forfeit' => 'boolean',
                        'participant_id' => 'required|exists:App\\Models\\Participant,id',
                        'round_id' => 'required|exists:App\\Models\\Round,id',
-                       'time' => 'required|numeric',
+                       'time' => 'required_if:forfeit,false|numeric',
                ]);
+               error_log(var_export($validatedData, true));
 
                $participant = Participant::findOrFail($validatedData['participant_id']);
                $round = Round::findOrFail($validatedData['round_id']);
@@ -31,7 +33,8 @@ class ResultController extends Controller
                        'round_id' => $validatedData['round_id'],
                        'user_id' => $participant->user_id,
                ], [
-                       'time' => $validatedData['time'],
+                       'forfeit' => $validatedData['forfeit'],
+                       'time' => isset($validatedData['time']) ? $validatedData['time'] : 0,
                ]);
 
                Protocol::resultReported(
index 8adc5456474352562c4cbbb36503dacc2cf8e5ca..ddbc441d40a12fe185fe82ac7f52ce9358b7a21f 100644 (file)
@@ -65,6 +65,7 @@ class Protocol extends Model
        protected static function resultMemo(Result $result) {
                return [
                        'id' => $result->id,
+                       'forfeit' => $result->forfeit,
                        'time' => $result->time,
                ];
        }
index e4954195bb3969ceb3be8510f2e085de276b9dcf..3ced393cc64b1d13ebe3d2970e399e48550a9c13 100644 (file)
@@ -18,7 +18,7 @@ class Result extends Model
        }
 
        public function getHasFinishedAttribute() {
-               return $this->time > 0;
+               return $this->time > 0 || $this->forfeit;
        }
 
        protected $appends = [
@@ -26,6 +26,7 @@ class Result extends Model
        ];
 
        protected $fillable = [
+               'forfeit',
                'round_id',
                'time',
                'user_id',
diff --git a/database/migrations/2022_03_15_081701_add_forfeit_result.php b/database/migrations/2022_03_15_081701_add_forfeit_result.php
new file mode 100644 (file)
index 0000000..82a8166
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        *
+        * @return void
+        */
+       public function up()
+       {
+               Schema::table('results', function(Blueprint $table) {
+                       $table->boolean('forfeit')->default(false);
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        *
+        * @return void
+        */
+       public function down()
+       {
+               Schema::table('results', function(Blueprint $table) {
+                       $table->dropColumn('forfeit');
+               });
+       }
+};
index dc091e127aa2cffd040bc38855940a75c529f6db..abdb0d544d183ef4cbb89db733cb3411716c1ddc 100644 (file)
@@ -11,7 +11,8 @@
     },
     "eslintConfig": {
         "env": {
-            "browser": true
+            "browser": true,
+                       "node": true
         },
         "extends": [
             "eslint:recommended",
@@ -23,6 +24,7 @@
             "sourceType": "module"
         },
         "rules": {
+                       "import/no-named-as-default-member": 0,
             "max-len": [
                 "warn",
                 {
index 453ad728ef895d5d7cb83180d25ce982934e4a35..cffa4cb5770b27615eb910c235faa0132b7943dc 100644 (file)
@@ -2,7 +2,9 @@ window._ = require('lodash');
 
 try {
     require('bootstrap');
-} catch (e) {}
+} catch (e) {
+       // well...
+}
 
 /**
  * We'll load the axios HTTP library which allows us to easily issue requests
index 003adff91b69a541655e709b28e14884f2122dbd..2ad4e512036d873a6200f6cea98f139665d3395a 100644 (file)
@@ -1,6 +1,5 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faJsSquare } from '@fortawesome/free-brands-svg-icons';
 import { fab } from '@fortawesome/free-brands-svg-icons';
 import { fas } from '@fortawesome/free-solid-svg-icons';
 import React from 'react';
@@ -9,7 +8,6 @@ import { withTranslation } from 'react-i18next';
 
 import i18n from '../../i18n';
 
-library.add(faJsSquare);
 library.add(fab);
 library.add(fas);
 
@@ -63,6 +61,7 @@ Icon.DISCORD = makePreset('DiscordIcon', ['fab', 'discord']);
 Icon.EDIT = makePreset('EditIcon', 'edit');
 Icon.FINISHED = makePreset('FinishedIcon', 'square-check');
 Icon.FIRST_PLACE = makePreset('FirstPlaceIcon', 'trophy');
+Icon.FORFEIT = makePreset('ForfeitIcon', 'square-xmark');
 Icon.LOGOUT = makePreset('LogoutIcon', 'sign-out-alt');
 Icon.PENDING = makePreset('PendingIcon', 'clock');
 Icon.PROTOCOL = makePreset('ProtocolIcon', 'file-alt');
diff --git a/resources/js/components/common/LargeCheck.js b/resources/js/components/common/LargeCheck.js
new file mode 100644 (file)
index 0000000..e159b45
--- /dev/null
@@ -0,0 +1,54 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Icon from './Icon';
+
+const LargeCheck = ({
+       className,
+       id,
+       name,
+       onBlur,
+       onChange,
+       value,
+}) => {
+       let clsn = className ? `${className} custom-check` : 'custom-check';
+       if (value) {
+               clsn += ' checked';
+       }
+       return <span
+               className={clsn}
+               id={id}
+               onBlur={onBlur ? () => onBlur({ target: { name, value } }) : null}
+               onClick={onChange ? () => onChange({ target: { name, value: !value } }) : null}
+               onKeyPress={onChange ? e => {
+                       if (e.key == 'Enter' || e.key == ' ') {
+                               e.preventDefault();
+                               e.stopPropagation();
+                               onChange({ target: { name, value: !value } });
+                       }
+               } : null}
+               tabIndex="0"
+       >
+               <Icon name="check" />
+       </span>;
+};
+
+LargeCheck.propTypes = {
+       className: PropTypes.string,
+       id: PropTypes.string,
+       name: PropTypes.string,
+       onBlur: PropTypes.func,
+       onChange: PropTypes.func,
+       value: PropTypes.bool,
+};
+
+LargeCheck.defaultProps = {
+       className: '',
+       id: '',
+       name: '',
+       onBlur: null,
+       onChange: null,
+       value: false,
+};
+
+export default LargeCheck;
index 2c1b65688416c6639925ef375d5255e7782a9d20..43ea91d09ca73157d299c9fa3a51075a17e145c9 100644 (file)
@@ -13,6 +13,9 @@ const getIcon = (result, index) => {
        if (!result || !result.has_finished) {
                return <Icon.PENDING className="text-muted" size="lg" />;
        }
+       if (result.forfeit) {
+               return <Icon.FORFEIT className="text-danger" size="lg" />;
+       }
        if (index === 0) {
                return <Icon.FIRST_PLACE className="text-gold" size="lg" />;
        }
index fc98c7eefe0fcb8ddddae3cea6fbbca6d0826a4b..e7d1d9dec2566eb6afca44578138e0d7179a7128 100644 (file)
@@ -4,8 +4,12 @@ import PropTypes from 'prop-types';
 import React from 'react';
 import { Button, Col, Form, Modal, Row } from 'react-bootstrap';
 import { withTranslation } from 'react-i18next';
+import toastr from 'toastr';
 
+import LargeCheck from '../common/LargeCheck';
 import i18n from '../../i18n';
+import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik';
+import { findResult } from '../../helpers/Participant';
 import { formatTime, parseTime } from '../../helpers/Result';
 import yup from '../../schema/yup';
 
@@ -21,14 +25,14 @@ const ReportForm = ({
 <Form noValidate onSubmit={handleSubmit}>
        <Modal.Body>
                <Row>
-                       <Form.Group as={Col} controlId="report.time">
+                       <Form.Group as={Col} sm={9} controlId="report.time">
                                <Form.Label>{i18n.t('results.reportTime')}</Form.Label>
                                <Form.Control
                                        isInvalid={!!(touched.time && errors.time)}
                                        name="time"
                                        onBlur={handleBlur}
                                        onChange={handleChange}
-                                       placeholder="1:22:59"
+                                       placeholder={values.forfeit ? 'DNF' : '1:22:59'}
                                        type="text"
                                        value={values.time || ''}
                                />
@@ -47,6 +51,17 @@ const ReportForm = ({
                                        </Form.Text>
                                }
                        </Form.Group>
+                       <Form.Group as={Col} sm={3} controlId="report.forfeit">
+                               <Form.Label>{i18n.t('results.forfeit')}</Form.Label>
+                               <Form.Control
+                                       as={LargeCheck}
+                                       isInvalid={!!(touched.forfeit && errors.forfeit)}
+                                       name="forfeit"
+                                       onBlur={handleBlur}
+                                       onChange={handleChange}
+                                       value={!!values.forfeit}
+                               />
+                       </Form.Group>
                </Row>
        </Modal.Body>
        <Modal.Footer>
@@ -63,6 +78,7 @@ const ReportForm = ({
 
 ReportForm.propTypes = {
        errors: PropTypes.shape({
+               forfeit: PropTypes.string,
                time: PropTypes.string,
        }),
        handleBlur: PropTypes.func,
@@ -70,9 +86,11 @@ ReportForm.propTypes = {
        handleSubmit: PropTypes.func,
        onCancel: PropTypes.func,
        touched: PropTypes.shape({
+               forfeit: PropTypes.bool,
                time: PropTypes.bool,
        }),
        values: PropTypes.shape({
+               forfeit: PropTypes.bool,
                time: PropTypes.string,
        }),
 };
@@ -81,23 +99,42 @@ export default withFormik({
        displayName: 'ReportForm',
        enableReinitialize: true,
        handleSubmit: async (values, actions) => {
-               const { participant_id, round_id, time } = values;
+               const { forfeit, participant_id, round_id, time } = values;
+               const { setErrors } = actions;
                const { onCancel } = actions.props;
-               await axios.post('/api/results', {
-                       participant_id,
-                       round_id,
-                       time: parseTime(time),
-               });
-               if (onCancel) {
-                       onCancel();
+               try {
+                       await axios.post('/api/results', {
+                               forfeit,
+                               participant_id,
+                               round_id,
+                               time: parseTime(time) || 0,
+                       });
+                       toastr.success(i18n.t('results.reportSuccess'));
+                       if (onCancel) {
+                               onCancel();
+                       }
+               } catch (e) {
+                       toastr.success(i18n.t('results.reportError'));
+                       if (e.response && e.response.data && e.response.data.errors) {
+                               setErrors(laravelErrorsToFormik(e.response.data.errors));
+                       }
                }
        },
-       mapPropsToValues: ({ participant, round }) => ({
-               participant_id: participant.id,
-               round_id: round.id,
-               time: '',
-       }),
+       mapPropsToValues: ({ participant, round }) => {
+               const result = findResult(participant, round);
+               console.log(result);
+               return {
+                       forfeit: result ? !!result.forfeit : false,
+                       participant_id: participant.id,
+                       round_id: round.id,
+                       time: result && result.time ? formatTime(result) : '',
+               };
+       },
        validationSchema: yup.object().shape({
-               time: yup.string().required().time(),
+               forfeit: yup.boolean().required(),
+               time: yup.string().time().when('forfeit', {
+                       is: false,
+                       then: yup.string().required().time(),
+               }),
        }),
 })(withTranslation()(ReportForm));
index e5a135f1b14c47f09182898f0d92aa8aaf5cfa13..3db4121c5b6d3689aca5911b9388c8af4d0cc7ac 100644 (file)
@@ -1,8 +1,8 @@
 export const compareResult = round => (a, b) => {
        const a_result = findResult(a, round);
        const b_result = findResult(b, round);
-       const a_time = a_result ? a_result.time : 0;
-       const b_time = b_result ? b_result.time : 0;
+       const a_time = a_result && !a_result.forfeit ? a_result.time : 0;
+       const b_time = b_result && !b_result.forfeit ? b_result.time : 0;
        if (a_time) {
                if (b_time) {
                        if (a_time < b_time) return -1;
@@ -14,6 +14,17 @@ export const compareResult = round => (a, b) => {
        if (b_time) {
                return 1;
        }
+       const a_forfeit = a_result && a_result.forfeit;
+       const b_forfeit = b_result && b_result.forfeit;
+       if (a_forfeit) {
+               if (b_forfeit) {
+                       return 0;
+               }
+               return -1;
+       }
+       if (b_forfeit) {
+               return 1;
+       }
        return 0;
 };
 
index 4de0aaee0afebbbc5d2aaebffb2fd8bd6f9e1e72..06ade091803e4a649a14914247e761ac01ad9619 100644 (file)
@@ -13,7 +13,7 @@ export const formatTime = result => {
 
 export const parseTime = str => {
        if (!str) return null;
-       return `${str}`.split(/[-\.: ]+/).reduce((acc,time) => (60 * acc) + +time, 0);
+       return `${str}`.split(/[-.: ]+/).reduce((acc,time) => (60 * acc) + +time, 0);
 };
 
 export default {
diff --git a/resources/js/helpers/laravelErrorsToFormik.js b/resources/js/helpers/laravelErrorsToFormik.js
new file mode 100644 (file)
index 0000000..477627d
--- /dev/null
@@ -0,0 +1,7 @@
+const laravelErrorsToFormik = errors =>
+       Object.keys(errors || {}).reduce((result, key) => ({
+               ...result,
+               [key]: errors[key].join(', '),
+       }), {});
+
+export default laravelErrorsToFormik;
index c52500056f788d8c5adf0a29abb27f3b71eb9dad..7fbcf66bcc598eaf4dc8200e5d0add5629d95649 100644 (file)
@@ -47,9 +47,12 @@ export default {
                },
                results: {
                        edit: 'Ergebnis ändern',
+                       forfeit: 'Aufgegeben',
                        report: 'Ergebnis eintragen',
-                       reportTime: 'Zeit',
+                       reportError: 'Fehler beim Eintragen :(',
                        reportPreview: 'Wird als {{ time }} festgehalten',
+                       reportSuccess: 'Ergebnis festgehalten',
+                       reportTime: 'Zeit',
                        time: 'Zeit: {{ time }}',
                },
                rounds: {
index 19f8b5824e1b978a62314f319c1727d5db8db86b..bc506bc4004c394c655cfc64c3db9a2e0d6f626b 100644 (file)
@@ -6,6 +6,7 @@ yup.addMethod(yup.string, 'time', function (errorMessage) {
        return this.test('test-time-format', errorMessage, function (value) {
                const { path, createError } = this;
                return (
+                       !value ||
                        parseTime(value) ||
                        createError({ path, message: errorMessage || 'validation.error.time' })
                );
index cbb7c679bde7a2574def771e86ce7c4d62af509b..b19f13e2f5599b494afade01bd3dfffb8b30af46 100644 (file)
@@ -12,6 +12,7 @@
 
 // Custom
 @import 'common';
+@import 'form';
 @import 'participants';
 @import 'results';
 @import 'rounds';
diff --git a/resources/sass/form.scss b/resources/sass/form.scss
new file mode 100644 (file)
index 0000000..b44ae81
--- /dev/null
@@ -0,0 +1,10 @@
+.custom-check {
+       display: table;
+       width: auto;
+       > * {
+               visibility: hidden;
+       }
+       &.checked > * {
+               visibility: visible;
+       }
+}