]> git.localhorst.tv Git - alttp.git/commitdiff
basic group interface implementation
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Tue, 25 Nov 2025 11:17:29 +0000 (12:17 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Tue, 25 Nov 2025 11:17:29 +0000 (12:17 +0100)
17 files changed:
resources/js/components/groups/Interface.jsx [new file with mode: 0644]
resources/js/components/groups/Item.jsx [new file with mode: 0644]
resources/js/components/groups/List.jsx [new file with mode: 0644]
resources/js/components/protocol/Item.jsx
resources/js/components/results/Badge.jsx [new file with mode: 0644]
resources/js/components/results/Item.jsx
resources/js/components/results/List.jsx
resources/js/components/rounds/Item.jsx
resources/js/components/tournament/Detail.jsx
resources/js/components/tournament/GroupInterface.jsx [deleted file]
resources/js/helpers/Tournament.js
resources/js/helpers/permissions.js
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/sass/app.scss
resources/sass/groups.scss [new file with mode: 0644]
resources/sass/results.scss

diff --git a/resources/js/components/groups/Interface.jsx b/resources/js/components/groups/Interface.jsx
new file mode 100644 (file)
index 0000000..e16e9d7
--- /dev/null
@@ -0,0 +1,38 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import List from './List';
+import { getAssignedRounds, missingGroupAssignment } from '../../helpers/Tournament';
+import { useUser } from '../../hooks/user';
+
+const GroupInterface = ({ selfAssign, tournament }) => {
+       const { t } = useTranslation();
+       const { user } = useUser();
+
+       const assignedRounds = React.useMemo(() => getAssignedRounds(tournament, user), [tournament, user]);
+
+       if (!user) {
+               return <div><p>{t('groups.loginRequired')}</p></div>
+       }
+
+       if (missingGroupAssignment(tournament, user)) {
+               return <div>
+                       <p>{t('groups.missingAssignments')}</p>
+                       <Button onClick={selfAssign}>
+                               {t('groups.selfAssignButton')}
+                       </Button>
+               </div>
+       }
+
+       return <List rounds={assignedRounds} tournament={tournament} />;
+};
+
+GroupInterface.propTypes = {
+       selfAssign: PropTypes.func,
+       tournament: PropTypes.shape({
+       }),
+};
+
+export default GroupInterface;
diff --git a/resources/js/components/groups/Item.jsx b/resources/js/components/groups/Item.jsx
new file mode 100644 (file)
index 0000000..a7b3a23
--- /dev/null
@@ -0,0 +1,92 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import Icon from '../common/Icon';
+import Badge from '../results/Badge';
+import ReportButton from '../results/ReportButton';
+import SeedButton from '../rounds/SeedButton';
+import SeedCode from '../rounds/SeedCode';
+import { mayReportResult } from '../../helpers/permissions';
+import { formatNumberAlways } from '../../helpers/Round';
+import { findResult } from '../../helpers/User';
+import { useUser } from '../../hooks/user';
+
+const getStatusIcon = (round, result, t) => {
+       if (!result) {
+               return <Button className="group-status" title={t('groups.pendingResult')} variant="outline-secondary">
+                       <Icon.PENDING title="" />
+               </Button>;
+       }
+       if (!result.vod) {
+               return <Button className="group-status" title={t('groups.missingVod')} variant="outline-warning">
+                       <Icon.WARNING title="" />
+               </Button>;
+       }
+       return <Button className="group-status" title={t('groups.complete')} variant="success">
+               <Icon.FINISHED />
+       </Button>;
+}
+
+const Item = ({
+       round,
+       tournament,
+}) => {
+       const { t } = useTranslation();
+       const { user } = useUser();
+
+       const result = React.useMemo(
+               () => findResult(user, round),
+               [round, user],
+       );
+
+       return <li className="group">
+               <h3 className="group-title">
+                       {round.title ?
+                               round.title
+                       :
+                               `${formatNumberAlways(tournament, round)} ${t('rounds.date', { date: new Date(round.created_at) })}`
+                       }
+               </h3>
+               <div className="group-details">
+                       {getStatusIcon(round, result, t)}
+                       <div className="group-seed">
+                               {round.code && round.code.length ?
+                                       <SeedCode code={round.code} game={round.game || 'alttpr'} />
+                               : null}
+                               <SeedButton round={round} tournament={tournament} />
+                       </div>
+                       <div className="group-result">
+                               {mayReportResult(user, tournament) ?
+                                       <ReportButton round={round} tournament={tournament} user={user} />
+                               : null}
+                               <Badge round={round} tournament={tournament} user={user} />
+                       </div>
+               </div>
+       </li>;
+};
+
+Item.propTypes = {
+       round: PropTypes.shape({
+               code: PropTypes.arrayOf(PropTypes.string),
+               created_at: PropTypes.string,
+               game: PropTypes.string,
+               id: PropTypes.number,
+               locked: PropTypes.bool,
+               number: PropTypes.number,
+               results: PropTypes.arrayOf(PropTypes.shape({
+               })),
+               seed: PropTypes.string,
+               title: PropTypes.string,
+       }),
+       tournament: PropTypes.shape({
+               participants: PropTypes.arrayOf(PropTypes.shape({
+               })),
+               id: PropTypes.number,
+               show_numbers: PropTypes.bool,
+               type: PropTypes.string,
+       }),
+};
+
+export default Item;
diff --git a/resources/js/components/groups/List.jsx b/resources/js/components/groups/List.jsx
new file mode 100644 (file)
index 0000000..e69c8ff
--- /dev/null
@@ -0,0 +1,42 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Alert } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+
+import Item from './Item';
+import LoadMore from '../rounds/LoadMore';
+import i18n from '../../i18n';
+
+const List = ({
+       loadMore,
+       rounds,
+       tournament,
+}) => rounds && rounds.length ? <>
+       <ol className="groups">
+               {rounds.map(round =>
+                       <Item
+                               key={round.id}
+                               round={round}
+                               tournament={tournament}
+                       />
+               )}
+       </ol>
+       {loadMore ?
+               <LoadMore loadMore={loadMore} />
+       : null}
+</> :
+       <Alert variant="info">
+               {i18n.t('groups.empty')}
+       </Alert>
+;
+
+List.propTypes = {
+       loadMore: PropTypes.func,
+       rounds: PropTypes.arrayOf(PropTypes.shape({
+               id: PropTypes.number,
+       })),
+       tournament: PropTypes.shape({
+       }),
+};
+
+export default withTranslation()(List);
index 29bec88344c7019bddee870b0ea721f68fdd7a3c..e32c28faad60db0b1db034042bbfb1272dd3321c 100644 (file)
@@ -16,6 +16,11 @@ const getEntryDate = entry => {
                : dateStr;
 };
 
+const getEntryDetailsAssignee = entry => {
+       if (!entry || !entry.details || !entry.details.assignee) return 'Anonymous';
+       return getUserName(entry.details.assignee);
+};
+
 const getEntryDetailsUsername = entry => {
        if (!entry || !entry.details || !entry.details.user) return 'Anonymous';
        return getUserName(entry.details.user);
@@ -62,8 +67,8 @@ const getEntryDescription = (entry, t) => {
                                `protocol.description.${entry.type}`,
                                {
                                        ...entry,
+                                       assignee: getEntryDetailsAssignee(entry),
                                        picks: getEntryDetailsPicks(entry),
-                                       username: getEntryDetailsUsername(entry),
                                },
                        );
                case 'result.comment': {
diff --git a/resources/js/components/results/Badge.jsx b/resources/js/components/results/Badge.jsx
new file mode 100644 (file)
index 0000000..1339d82
--- /dev/null
@@ -0,0 +1,72 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from 'react-bootstrap';
+
+import DetailDialog from './DetailDialog';
+import { getIcon, getTime } from '../../helpers/Result';
+import { maySeeResult } from '../../helpers/permissions';
+import { findResult } from '../../helpers/User';
+import { useUser } from '../../hooks/user';
+
+const getClassName = result => {
+       const classNames = ['result-badge', 'status'];
+       if (result && result.has_finished) {
+               classNames.push('finished');
+               if (result.comment) {
+                       classNames.push('has-comment');
+               }
+       } else {
+               classNames.push('pending');
+       }
+       return classNames.join(' ');
+};
+
+const Badge = ({
+       round,
+       tournament,
+       user,
+}) => {
+       const [showDialog, setShowDialog] = React.useState(false);
+
+       const { user: authUser } = useUser();
+
+       const result = React.useMemo(
+               () => findResult(user, round),
+               [round, user],
+       );
+       const maySee = React.useMemo(
+               () => maySeeResult(authUser, tournament, round, result),
+               [authUser, result, round, tournament],
+       );
+
+       return <>
+               <Button
+                       className={getClassName(result)}
+                       onClick={() => setShowDialog(true)}
+                       title={maySee && result && result.comment ? result.comment : null}
+               >
+                       <span className="time">
+                               {getTime(result, maySee)}
+                       </span>
+                       {getIcon(result, maySeeResult(authUser, tournament, round))}
+               </Button>
+               <DetailDialog
+                       onHide={() => setShowDialog(false)}
+                       round={round}
+                       show={showDialog}
+                       tournament={tournament}
+                       user={user}
+               />
+       </>;
+};
+
+Badge.propTypes = {
+       round: PropTypes.shape({
+       }),
+       tournament: PropTypes.shape({
+       }),
+       user: PropTypes.shape({
+       }),
+};
+
+export default Badge;
index da0c99e88f9c21289837fb5fda359ce7967c32e8..b3a0b898707a25c9c0b58a102173f617dff088e6 100644 (file)
@@ -1,29 +1,15 @@
 import PropTypes from 'prop-types';
-import React, { useState } from 'react';
+import React from 'react';
 import { Button } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 
-import DetailDialog from './DetailDialog';
+import Badge from './Badge';
 import Icon from '../common/Icon';
 import Box from '../users/Box';
-import { getIcon, getTime } from '../../helpers/Result';
 import { maySeeResult } from '../../helpers/permissions';
 import { findResult } from '../../helpers/User';
 import { useUser } from '../../hooks/user';
 
-const getClassName = result => {
-       const classNames = ['status'];
-       if (result && result.has_finished) {
-               classNames.push('finished');
-               if (result.comment) {
-                       classNames.push('has-comment');
-               }
-       } else {
-               classNames.push('pending');
-       }
-       return classNames.join(' ');
-};
-
 const twitchReg = /^https?:\/\/(www\.)?twitch\.tv/;
 const youtubeReg = /^https?:\/\/(www\.)?youtu(\.be|be\.)/;
 
@@ -54,8 +40,6 @@ const Item = ({
        tournament,
        user,
 }) => {
-       const [showDialog, setShowDialog] = useState(false);
-
        const { t } = useTranslation();
        const { user: authUser } = useUser();
 
@@ -71,16 +55,7 @@ const Item = ({
        return <div className="result">
                <Box user={user} />
                <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, maySeeResult(authUser, tournament, round))}
-                       </Button>
+                       <Badge round={round} tournament={tournament} user={user} />
                        {maySee && result && result.vod ?
                                <Button
                                        className="vod-link"
@@ -94,13 +69,6 @@ const Item = ({
                                </Button>
                        : null}
                </div>
-               <DetailDialog
-                       onHide={() => setShowDialog(false)}
-                       round={round}
-                       show={showDialog}
-                       tournament={tournament}
-                       user={user}
-               />
        </div>;
 };
 
index f26a1310a3d48271b6caef2e8b5a6fa6166b77ba..06d34d825ada795567632a2539926a7137ad2657 100644 (file)
@@ -4,38 +4,39 @@ import React from 'react';
 import Item from './Item';
 import { sortByFinished, sortByResult } from '../../helpers/Participant';
 import { maySeeResults } from '../../helpers/permissions';
-import { getRunners } from '../../helpers/Tournament';
+import { getRunners, hasFixedRunners } from '../../helpers/Tournament';
 import { sortByTime, sortByUsername } from '../../helpers/Result';
 import { useUser } from '../../hooks/user';
 
 const List = ({ round, tournament }) => {
        const { user } = useUser();
 
-       if (tournament.type === 'open-async') {
-               const results = maySeeResults(user, tournament, round)
-                       ? sortByTime(round.results || [])
-                       : sortByUsername(round.results || []);
+       if (hasFixedRunners(tournament)) {
+               const runners = maySeeResults(user, tournament, round)
+                       ? sortByResult(getRunners(tournament), round)
+                       : sortByFinished(getRunners(tournament), round);
                return <div className="results d-flex flex-wrap">
-                       {results.map(result =>
+                       {runners.map(participant =>
                                <Item
-                                       key={result.id}
+                                       key={participant.id}
                                        round={round}
                                        tournament={tournament}
-                                       user={result.user}
+                                       user={participant.user}
                                />
                        )}
                </div>;
        }
-       const runners = maySeeResults(user, tournament, round)
-               ? sortByResult(getRunners(tournament), round)
-               : sortByFinished(getRunners(tournament), round);
-       return <div className="results d-flex flex-wrap">
-               {runners.map(participant =>
+
+       const results = maySeeResults(user, tournament, round)
+               ? sortByTime(round.results || [])
+               : sortByUsername(round.results || []);
+       return <div className="results d-flex flex-wrap align-content-start">
+               {results.map(result =>
                        <Item
-                               key={participant.id}
+                               key={result.id}
                                round={round}
                                tournament={tournament}
-                               user={participant.user}
+                               user={result.user}
                        />
                )}
        </div>;
index 39f45862b255dd410d24a9dba7a1b0691ab2e92c..2d207f2df75c9ab452f1a744764022a13349d861 100644 (file)
@@ -19,6 +19,7 @@ import {
        isRunner,
 } from '../../helpers/permissions';
 import { formatNumber, isComplete } from '../../helpers/Round';
+import { hasFixedRunners } from '../../helpers/Tournament';
 import { hasFinishedRound } from '../../helpers/User';
 import { useUser } from '../../hooks/user';
 
@@ -83,7 +84,7 @@ const Item = ({
                                        </p>
                                : null}
                                <div className="bottom-half">
-                                       {tournament.type === 'open-async' && round.results && round.results.length ?
+                                       {!hasFixedRunners(tournament) && round.results && round.results.length ?
                                                <p>{t('rounds.numberOfResults', { count: round.results.length })}</p>
                                        : null}
                                        <div className="button-bar">
index 281a2455c4d786840046a2a11586211124c5a45d..2407d9bc90aa52494acd24a4e87ba6cba38424d4 100644 (file)
@@ -5,13 +5,13 @@ import { useTranslation } from 'react-i18next';
 
 import ApplyButton from './ApplyButton';
 import ExportButton from './ExportButton';
-import GroupInterface from './GroupInterface';
 import Scoreboard from './Scoreboard';
 import ScoreChartButton from './ScoreChartButton';
 import SettingsButton from './SettingsButton';
 import ApplicationsButton from '../applications/Button';
 import Icon from '../common/Icon';
 import RawHTML from '../common/RawHTML';
+import GroupInterface from '../groups/Interface';
 import Protocol from '../protocol/Protocol';
 import Rounds from '../rounds/List';
 import Box from '../users/Box';
@@ -132,12 +132,15 @@ const Detail = ({
                                </div>
                        </Col>
                        <Col lg={{ order: 1, span: 8 }} xl={{ order: 1, span: 9 }}>
-                               {hasAssignedGroups(tournament) ?
-                                       <GroupInterface
-                                               selfAssign={actions.selfAssignGroups}
-                                               tournament={tournament}
-                                       />
-                               : null}
+                               {hasAssignedGroups(tournament) ? (
+                                       <div className="mb-3">
+                                               <h2>{t('groups.heading')}</h2>
+                                               <GroupInterface
+                                                       selfAssign={actions.selfAssignGroups}
+                                                       tournament={tournament}
+                                               />
+                                       </div>
+                               ): null}
                                <div className="d-flex align-items-center justify-content-between">
                                        <h2>{t('rounds.heading')}</h2>
                                        {actions.addRound && mayAddRounds(user, tournament) ?
diff --git a/resources/js/components/tournament/GroupInterface.jsx b/resources/js/components/tournament/GroupInterface.jsx
deleted file mode 100644 (file)
index aed89d2..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import { Button } from 'react-bootstrap';
-import { useTranslation } from 'react-i18next';
-
-import { missingGroupAssignment } from '../../helpers/Tournament';
-import { useUser } from '../../hooks/user';
-
-const GroupInterface = ({ selfAssign, tournament }) => {
-       const { t } = useTranslation();
-       const { user } = useUser();
-
-       if (!user) {
-               return <div><p>{t('groups.loginRequired')}</p></div>
-       }
-
-       if (missingGroupAssignment(tournament, user)) {
-               return <div>
-                       <p>{t('groups.missingAssignments')}</p>
-                       <Button onClick={selfAssign}>
-                               {t('groups.selfAssignButton')}
-                       </Button>
-               </div>
-       }
-
-       return <div>
-               Groups here
-       </div>;
-};
-
-GroupInterface.propTypes = {
-       selfAssign: PropTypes.func,
-       tournament: PropTypes.shape({
-       }),
-};
-
-export default GroupInterface;
index a96ab3649f076c96d0732c26c25a654f1acd40e8..c589ed4437519f58ed4f9f88dc89133e8c434641 100644 (file)
@@ -149,6 +149,15 @@ export const findParticipant = (tournament, user) => {
        return tournament.participants.find(p => p.user_id == user.id);
 };
 
+export const getAssignedRounds = (tournament, user) => {
+       if (!tournament?.group_assignments || !user) return [];
+       return tournament.rounds.filter(
+               (round) => tournament.group_assignments.find(
+                       (ga) => round.number === ga.round_number && round.group === ga.group,
+               ),
+       );
+};
+
 export const getPendingApplications = tournament => {
        if (!tournament || !tournament.applications || !tournament.applications.length) return [];
        return tournament.applications
@@ -165,6 +174,8 @@ export const getRunners = tournament => {
 
 export const hasAssignedGroups = tournament => (tournament?.type === 'open-grouped-async');
 
+export const hasFixedRunners = tournament => !['open-async', 'open-grouped-async'].includes(tournament?.type);
+
 export const hasScoreboard = tournament => !!(tournament && tournament.type === 'signup-async');
 
 export const hasSignup = tournament => !!(tournament && tournament.type === 'signup-async');
index d1d1952e407a259de0957c5527e2841e44816196..0c1aaf6fb86b5d6f91722d977d70ca7b81b2590b 100644 (file)
@@ -157,7 +157,7 @@ export const mayHandleApplications = (user, tournament) =>
 
 export const mayReportResult = (user, tournament) => {
        if (!user || !tournament) return false;
-       if (tournament.type === 'open-async') return true;
+       if (['open-async', 'open-grouped-async'].includes(tournament.type)) return true;
        return isRunner(user, tournament);
 };
 
index 5497a0b73ea5808a9048441c8bd9aa9a0e6de064..65990629f9a8ee9f41c958baf4032fc6a802a4e6 100644 (file)
@@ -380,8 +380,12 @@ export default {
                        uploading: 'Am Hochladen...',
                },
                groups: {
+                       complete: 'Abgeschlossen',
+                       heading: 'Gruppen',
                        loginRequired: 'Dieses Turnier nutzt Gruppenzuweisung. Bitte melde dich an, um deine Seeds zu laden.',
                        missingAssignments: 'Dieses Turnier nutzt Gruppenzuweisung. Falls du teilnehmen möchtest, hol dir bitter hier deine Zuweisungen ab.',
+                       missingVod: 'VoD fehlt',
+                       pendingResult: 'Ergebnis ausstehend',
                        selfAssignButton: 'Gruppen zuweisen',
                        selfAssignError: 'Fehler beim Zuweisen',
                        selfAssignSuccess: 'Gruppen zugewiesen',
@@ -569,7 +573,7 @@ export default {
                                        rejected: 'Anmeldung von {{username}} abgelehnt',
                                },
                                group: {
-                                       assign: 'Gruppen {{picks}} für {{username}} zugewiesen',
+                                       assign: 'Gruppen {{picks}} für {{assignee}} zugewiesen',
                                },
                                result: {
                                        comment: 'Ergebnis von Runde {{number}} kommentiert: <1>{{comment}}</1>',
index a3a3c3fa1b2ede9eb2dbcf66b2a6e18a44e371c1..7638aa768c51b3e684b6d1ad2d5e7d6e996c0a88 100644 (file)
@@ -380,8 +380,12 @@ export default {
                        uploading: 'Uploading...',
                },
                groups: {
+                       complete: 'Complete',
+                       heading: 'Groups',
                        loginRequired: 'This tournament uses assigned groups. Please sign in to obtain your seeds.',
                        missingAssignments: 'This tournament uses assigned groups. If you want to participate, please grab your assignments here.',
+                       missingVod: 'Missing VoD',
+                       pendingResult: 'Pending result',
                        selfAssignButton: 'Assign groups',
                        selfAssignError: 'Error assigning groups',
                        selfAssignSuccess: 'Groups assigned',
@@ -569,7 +573,7 @@ export default {
                                        rejected: 'Application from {{username}} rejected',
                                },
                                group: {
-                                       assign: 'Assigned groups {{picks}} for {{username}}',
+                                       assign: 'Assigned groups {{picks}} for {{assignee}}',
                                },
                                result: {
                                        comment: 'Result of round {{number}} commented: <1>{{comment}}</1>',
index 910838f82aaa17aad9e697123a8628407601c31a..c7b80b81958b5c88afec9ee1b2d562a5fee6ea38 100644 (file)
@@ -14,6 +14,7 @@
 @import 'events';
 @import 'form';
 @import 'front';
+@import 'groups';
 @import 'map';
 @import 'participants';
 @import 'results';
diff --git a/resources/sass/groups.scss b/resources/sass/groups.scss
new file mode 100644 (file)
index 0000000..d76df89
--- /dev/null
@@ -0,0 +1,28 @@
+.groups {
+       margin: 1rem 0;
+       padding: 0;
+
+       .group {
+               margin: 1rem 0;
+               border: $border-width solid $secondary;
+               border-radius: $border-radius;
+               background: $gray-700;
+               padding: 1ex;
+               list-style: none;
+       }
+
+       .group-details {
+               display: flex;
+               gap: 1em;
+       }
+       .group-result {
+               margin-left: auto;
+               text-align: right;
+       }
+       .seed-code {
+               margin-right: 1ex;
+       }
+       .result-badge {
+               margin-left: 1ex;
+       }
+}
index b1ef0537bb8e746372b7dd6a8e2e280dedac6b67..5af982d710fefc5e52b294322d0ae0897499d700 100644 (file)
@@ -1,3 +1,39 @@
+.result-badge {
+       position: relative;
+       display: inline-flex;
+       align-items: center;
+       justify-content: space-between;
+       padding: 0.5em;
+       min-width: 15ex;
+       border: none;
+       border-radius: 1ex;
+       background: $dark !important;
+       color: $light;
+       box-shadow: none !important;
+       transition: top 0.15s ease-in-out;
+
+       &.has-comment {
+               box-shadow: 0 0.5ex 0 $info;
+       }
+       &:active {
+               box-shadow: none;
+               top: 0.5ex;
+       }
+       &:focus {
+               outline: medium dashed $light;
+       }
+
+       .time {
+               min-width: 9ex;
+               height: 1.6em;
+               border-radius: 0.5ex;
+               padding: 0 0.5ex;
+       }
+       &.finished .time {
+               background: $secondary;
+       }
+}
+
 .results {
        .result {
                padding: 1ex;
@@ -7,41 +43,9 @@
                }
 
                .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;
-                       }
-                       &:focus {
-                               outline: medium dashed $light;
-                       }
 
-                       .time {
-                               min-width: 9ex;
-                               height: 1.6em;
-                               border-radius: 0.5ex;
-                               padding: 0 0.5ex;
-                       }
-                       &.finished .time {
-                               background: $secondary;
-                       }
                }
 
                .vod-link {