'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']);
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()) {
$request->user(),
);
DiscordBotCommand::queueResult($result);
- } else if ($result->wasChanged('comment')) {
+ } else if ($result->wasChanged(['comment', 'vod'])) {
Protocol::resultCommented(
$round->tournament,
$result,
--- /dev/null
+<?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');
+ });
+ }
+};
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;
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'];
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,
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}
}),
};
-export default withUser(Item, 'authUser');
+export default withTranslation()(withUser(Item, 'authUser'));
</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 ?
comment: PropTypes.string,
forfeit: PropTypes.string,
time: PropTypes.string,
+ vod: PropTypes.string,
}),
handleBlur: PropTypes.func,
handleChange: PropTypes.func,
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,
}),
};
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 {
round_id,
time: parseTime(time) || 0,
user_id,
+ vod,
});
toastr.success(i18n.t('results.reportSuccess'));
if (onCancel) {
round_id: round.id,
time: result && result.time ? formatTime(result) : '',
user_id: user.id,
+ vod: result && result.vod ? result.vod : '',
};
},
validationSchema: yup.object().shape({
is: false,
then: yup.string().required().time(),
}),
+ vod: yup.string().url(),
}),
})(withTranslation()(ReportForm));
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>
size="sm"
target="_blank"
title={i18n.t('users.stream')}
- variant="outline-twitch"
+ variant={getStreamVariant(participant)}
>
- <Icon.STREAM title="" />
+ {getStreamIcon(participant)}
</Button>
: null}
</div>
SettingsIcon: 'Einstellungen',
StreamIcon: 'Stream',
ThirdPlaceIcon: 'Dritter Platz',
+ TwitchIcon: 'Twitch',
UnlockedIcon: 'Offen',
+ YoutubeIcon: 'YouTube',
+ VideoIcon: 'Video',
zelda: {
'big-key': 'Big Key',
'blue-boomerang': 'Boomerang',
round: 'Runde',
runner: 'Runner',
time: 'Zeit: {{ time }}',
+ vod: 'VoD',
+ vodNote: 'Falls ihr euer VoD teilen wollte, gerne hier rein.',
},
rounds: {
code: 'Code',
SettingsIcon: 'Settings',
StreamIcon: 'Stream',
ThirdPlaceIcon: 'Third Place',
+ TwitchIcon: 'Twitch',
UnlockedIcon: 'Unlocked',
+ YoutubeIcon: 'YouTube',
+ VideoIcon: 'Video',
zelda: {
'big-key': 'Big Key',
'blue-boomerang': 'Boomerang',
round: 'Round',
runner: 'Runner',
time: 'Time: {{ time }}',
+ vod: 'VoD',
+ vodNote: 'If you want to share your VoD, go ahead.',
},
rounds: {
code: 'Code',
$gold: #c9b037;
$silver: #b4b4b4;
$twitch: #6441a5;
+$youtube: #ff0000;
// Custom variant
$custom-colors: (
"discord": $discord,
- "twitch": $twitch
+ "twitch": $twitch,
+ "youtube": $youtube
);
background: $secondary;
}
}
+
+ .vod-link {
+ margin-top: 1ex;
+ margin-left: 0.5ex;
+ padding: 0.5em;
+ border-radius: 1ex;
+ }
}
}