]> git.localhorst.tv Git - alttp.git/commitdiff
add result VoD links
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 14 Dec 2022 11:50:50 +0000 (12:50 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 14 Dec 2022 11:50:50 +0000 (12:50 +0100)
app/Http/Controllers/ResultController.php
database/migrations/2022_12_14_093503_add_result_vod.php [new file with mode: 0644]
resources/js/components/common/Icon.js
resources/js/components/results/Item.js
resources/js/components/results/ReportForm.js
resources/js/components/tournament/Scoreboard.js
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/sass/_variables.scss
resources/sass/results.scss

index 57f78a157f2d1537cebc3bee96626cd629271a12..ba57c4b0759f45c33b0bb72fe776ec02cc65a5c7 100644 (file)
@@ -20,6 +20,7 @@ class ResultController extends Controller
                        'round_id' => 'required|exists:App\\Models\\Round,id',
                        'time' => 'numeric',
                        'user_id' => 'required|exists:App\\Models\\User,id',
+                       'vod' => 'string|url',
                ]);
 
                $round = Round::findOrFail($validatedData['round_id']);
@@ -36,7 +37,8 @@ class ResultController extends Controller
                        if (isset($validatedData['forfeit'])) $result->forfeit = $validatedData['forfeit'];
                        if (isset($validatedData['time'])) $result->time = $validatedData['time'];
                }
-               $result->comment = $validatedData['comment'] ? $validatedData['comment'] : null;
+               $result->comment = !empty($validatedData['comment']) ? $validatedData['comment'] : null;
+               $result->vod = !empty($validatedData['vod']) ? $validatedData['vod'] : null;
                $result->save();
 
                if ($result->wasChanged()) {
@@ -50,7 +52,7 @@ class ResultController extends Controller
                                $request->user(),
                        );
                        DiscordBotCommand::queueResult($result);
-               } else if ($result->wasChanged('comment')) {
+               } else if ($result->wasChanged(['comment', 'vod'])) {
                        Protocol::resultCommented(
                                $round->tournament,
                                $result,
diff --git a/database/migrations/2022_12_14_093503_add_result_vod.php b/database/migrations/2022_12_14_093503_add_result_vod.php
new file mode 100644 (file)
index 0000000..5b6c707
--- /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->text('vod')->nullable()->default(null);
+               });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+               Schema::table('results', function(Blueprint $table) {
+                       $table->dropColumn('vod');
+               });
+    }
+};
index 617ca0385d8997f217cbf3ba1bcf2d177c75f8ec..581ce6ff0da8d0ca2b77e186f13719db820b0cb0 100644 (file)
@@ -79,6 +79,9 @@ Icon.SECOND_PLACE = makePreset('SecondPlaceIcon', 'medal');
 Icon.SETTINGS = makePreset('SettingsIcon', 'cog');
 Icon.STREAM = makePreset('StreamIcon', ['fab', 'twitch']);
 Icon.THIRD_PLACE = makePreset('ThirdPlaceIcon', 'award');
+Icon.TWITCH = makePreset('TwitchIcon', ['fab', 'twitch']);
 Icon.UNLOCKED = makePreset('UnlockedIcon', 'lock-open');
+Icon.VIDEO = makePreset('VideoIcon', 'video');
+Icon.YOUTUBE = makePreset('YoutubeIcon', ['fab', 'youtube']);
 
 export default Icon;
index 4b243b34bc4a9a673a55c233d4c4c62481745851..b8a0ec61ec7f7ca23e42bbdae4b4c14eb5204d96 100644 (file)
@@ -1,13 +1,16 @@
 import PropTypes from 'prop-types';
 import React, { useState } from 'react';
 import { Button } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
 
 import DetailDialog from './DetailDialog';
+import Icon from '../common/Icon';
 import Box from '../users/Box';
 import { getIcon, getTime } from '../../helpers/Result';
 import { maySeeResults } from '../../helpers/permissions';
 import { findResult } from '../../helpers/User';
 import { withUser } from '../../helpers/UserContext';
+import i18n from '../../i18n';
 
 const getClassName = result => {
        const classNames = ['status'];
@@ -22,6 +25,31 @@ const getClassName = result => {
        return classNames.join(' ');
 };
 
+const twitchReg = /^https?:\/\/(www\.)?twitch\.tv/;
+const youtubeReg = /^https?:\/\/(www\.)?youtu(\.be|be\.)/;
+
+const getVoDVariant = result => {
+       if (!result || !result.vod) return 'outline-secondary';
+       if (twitchReg.test(result.vod)) {
+               return 'twitch';
+       }
+       if (youtubeReg.test(result.vod)) {
+               return 'outline-youtube';
+       }
+       return 'outline-secondary';
+};
+
+const getVoDIcon = result => {
+       const variant = getVoDVariant(result);
+       if (variant === 'twitch') {
+               return <Icon.TWITCH title="" />;
+       }
+       if (variant === 'outline-youtube') {
+               return <Icon.YOUTUBE title="" />;
+       }
+       return <Icon.VIDEO title="" />;
+};
+
 const Item = ({
        authUser,
        round,
@@ -33,16 +61,30 @@ const Item = ({
        const maySee = maySeeResults(authUser, tournament, round);
        return <div className="result">
                <Box user={user} />
-               <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)}
-               </Button>
+               <div className="d-flex align-items-center justify-content-between">
+                       <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)}
+                       </Button>
+                       {maySee && result && result.vod ?
+                               <Button
+                                       className="vod-link"
+                                       href={result.vod}
+                                       size="sm"
+                                       target="_blank"
+                                       title={i18n.t('results.vod')}
+                                       variant={getVoDVariant(result)}
+                               >
+                                       {getVoDIcon(result)}
+                               </Button>
+                       : null}
+               </div>
                <DetailDialog
                        onHide={() => setShowDialog(false)}
                        round={round}
@@ -64,4 +106,4 @@ Item.propTypes = {
        }),
 };
 
-export default withUser(Item, 'authUser');
+export default withTranslation()(withUser(Item, 'authUser'));
index 9940171f344f676edf3ce1d89388e4f789be5be7..35b7e0fddab94a55d5773c1c15b5cbc0371f4d8d 100644 (file)
@@ -66,19 +66,39 @@ const ReportForm = ({
                                </Form.Group>
                        </Row>
                : null}
-               <Row>
-                       <Form.Group as={Col} sm={12} controlId="report.comment">
-                               <Form.Label>{i18n.t('results.comment')}</Form.Label>
-                               <Form.Control
-                                       as="textarea"
-                                       isInvalid={!!(touched.comment && errors.comment)}
-                                       name="comment"
-                                       onBlur={handleBlur}
-                                       onChange={handleChange}
-                                       value={values.comment || ''}
-                               />
-                       </Form.Group>
-               </Row>
+               <Form.Group controlId="report.vod">
+                       <Form.Label>{i18n.t('results.vod')}</Form.Label>
+                       <Form.Control
+                               isInvalid={!!(touched.vod && errors.vod)}
+                               name="vod"
+                               onBlur={handleBlur}
+                               onChange={handleChange}
+                               placeholder="https://twitch.tv/youtube"
+                               type="text"
+                               value={values.vod || ''}
+                       />
+                       {touched.vod && errors.vod ?
+                               <Form.Control.Feedback type="invalid">
+                                       {i18n.t(errors.vod)}
+                               </Form.Control.Feedback>
+                       :
+                               <Form.Text muted>
+                                       {i18n.t('results.vodNote')}
+                               </Form.Text>
+                       }
+               </Form.Group>
+               <Form.Group controlId="report.comment">
+                       <Form.Label>{i18n.t('results.comment')}</Form.Label>
+                       <Form.Control
+                               as="textarea"
+                               isInvalid={!!(touched.comment && errors.comment)}
+                               name="comment"
+                               onBlur={handleBlur}
+                               onChange={handleChange}
+                               rows="6"
+                               value={values.comment || ''}
+                       />
+               </Form.Group>
        </Modal.Body>
        <Modal.Footer>
                {onCancel ?
@@ -97,6 +117,7 @@ ReportForm.propTypes = {
                comment: PropTypes.string,
                forfeit: PropTypes.string,
                time: PropTypes.string,
+               vod: PropTypes.string,
        }),
        handleBlur: PropTypes.func,
        handleChange: PropTypes.func,
@@ -109,11 +130,13 @@ ReportForm.propTypes = {
                comment: PropTypes.bool,
                forfeit: PropTypes.bool,
                time: PropTypes.bool,
+               vod: PropTypes.bool,
        }),
        values: PropTypes.shape({
                comment: PropTypes.string,
                forfeit: PropTypes.bool,
                time: PropTypes.string,
+               vod: PropTypes.string,
        }),
 };
 
@@ -121,7 +144,7 @@ export default withFormik({
        displayName: 'ReportForm',
        enableReinitialize: true,
        handleSubmit: async (values, actions) => {
-               const { comment, forfeit, round_id, time, user_id } = values;
+               const { comment, forfeit, round_id, time, user_id, vod } = values;
                const { setErrors } = actions;
                const { onCancel } = actions.props;
                try {
@@ -131,6 +154,7 @@ export default withFormik({
                                round_id,
                                time: parseTime(time) || 0,
                                user_id,
+                               vod,
                        });
                        toastr.success(i18n.t('results.reportSuccess'));
                        if (onCancel) {
@@ -151,6 +175,7 @@ export default withFormik({
                        round_id: round.id,
                        time: result && result.time ? formatTime(result) : '',
                        user_id: user.id,
+                       vod: result && result.vod ? result.vod : '',
                };
        },
        validationSchema: yup.object().shape({
@@ -160,5 +185,6 @@ export default withFormik({
                        is: false,
                        then: yup.string().required().time(),
                }),
+               vod: yup.string().url(),
        }),
 })(withTranslation()(ReportForm));
index 40ad82ff300724acda076a5f7038d1546b723c5e..f13adacb61bbaa7a4465f88fb8b549ea535cca40 100644 (file)
@@ -31,6 +31,33 @@ const getPlacementDisplay = participant => {
        return participant.placement;
 };
 
+const twitchReg = /^https?:\/\/(www\.)?twitch\.tv/;
+const youtubeReg = /^https?:\/\/(www\.)?youtu(\.be|be\.)/;
+
+const getStreamVariant = participant => {
+       if (!participant || !participant.user || !participant.user.stream_link) {
+               return 'outline-secondary';
+       }
+       if (twitchReg.test(participant.user.stream_link)) {
+               return 'outline-twitch';
+       }
+       if (youtubeReg.test(participant.user.stream_link)) {
+               return 'outline-youtube';
+       }
+       return 'outline-secondary';
+};
+
+const getStreamIcon = participant => {
+       const variant = getStreamVariant(participant);
+       if (variant === 'outline-twitch') {
+               return <Icon.TWITCH title="" />;
+       }
+       if (variant === 'outline-youtube') {
+               return <Icon.YOUTUBE title="" />;
+       }
+       return <Icon.VIDEO title="" />;
+};
+
 const Scoreboard = ({ tournament, user }) =>
 <Table striped className="scoreboard align-middle">
        <thead>
@@ -55,9 +82,9 @@ const Scoreboard = ({ tournament, user }) =>
                                                        size="sm"
                                                        target="_blank"
                                                        title={i18n.t('users.stream')}
-                                                       variant="outline-twitch"
+                                                       variant={getStreamVariant(participant)}
                                                >
-                                                       <Icon.STREAM title="" />
+                                                       {getStreamIcon(participant)}
                                                </Button>
                                        : null}
                                </div>
index a2c48371752c60e7fc60a18ea316999dcd2b32ba..f794876a041dd3bad29cb9011c2f00a16c570add 100644 (file)
@@ -300,7 +300,10 @@ export default {
                        SettingsIcon: 'Einstellungen',
                        StreamIcon: 'Stream',
                        ThirdPlaceIcon: 'Dritter Platz',
+                       TwitchIcon: 'Twitch',
                        UnlockedIcon: 'Offen',
+                       YoutubeIcon: 'YouTube',
+                       VideoIcon: 'Video',
                        zelda: {
                                'big-key': 'Big Key',
                                'blue-boomerang': 'Boomerang',
@@ -423,6 +426,8 @@ export default {
                        round: 'Runde',
                        runner: 'Runner',
                        time: 'Zeit: {{ time }}',
+                       vod: 'VoD',
+                       vodNote: 'Falls ihr euer VoD teilen wollte, gerne hier rein.',
                },
                rounds: {
                        code: 'Code',
index 9ef591644f23e82db72f78452826b902e6a2e4f0..dbe37f15765215db510f0e7adc55429860072a42 100644 (file)
@@ -300,7 +300,10 @@ export default {
                        SettingsIcon: 'Settings',
                        StreamIcon: 'Stream',
                        ThirdPlaceIcon: 'Third Place',
+                       TwitchIcon: 'Twitch',
                        UnlockedIcon: 'Unlocked',
+                       YoutubeIcon: 'YouTube',
+                       VideoIcon: 'Video',
                        zelda: {
                                'big-key': 'Big Key',
                                'blue-boomerang': 'Boomerang',
@@ -423,6 +426,8 @@ export default {
                        round: 'Round',
                        runner: 'Runner',
                        time: 'Time: {{ time }}',
+                       vod: 'VoD',
+                       vodNote: 'If you want to share your VoD, go ahead.',
                },
                rounds: {
                        code: 'Code',
index 886d1d2bc307ab5cd666e1794660cd43cd1b0486..62d7e8f9d1b595643dfd7f1fad7ff5b8148e0be3 100644 (file)
@@ -12,9 +12,11 @@ $discord: #5865f2;
 $gold: #c9b037;
 $silver: #b4b4b4;
 $twitch: #6441a5;
+$youtube: #ff0000;
 
 // Custom variant
 $custom-colors: (
        "discord": $discord,
-       "twitch": $twitch
+       "twitch": $twitch,
+       "youtube": $youtube
 );
index 3907e0ecd990f9d481737180b143b75cbcdd52e3..32f604eaf7f4d580f7be4ce24637c6b547b75d93 100644 (file)
                                background: $secondary;
                        }
                }
+
+               .vod-link {
+                       margin-top: 1ex;
+                       margin-left: 0.5ex;
+                       padding: 0.5em;
+                       border-radius: 1ex;
+               }
        }
 }