}
public function single(Request $request, $id) {
- $user = User::findOrFail($id);
+ $user = is_numeric($id) ? User::findOrFail($id) : User::where('username', '=', $id)->firstOrFail();
$this->authorize('view', $user);
+
+ $validatedData = $request->validate([
+ 'with' => 'array',
+ 'with.*' => 'string|in:episodes_as_comms,episodes_as_crew,episodes_as_setup,episodes_as_tracker,episodes_as_runner',
+ ]);
+
$user->append('random_quote');
- $user->load('participation');
- $user->load('participation.tournament');
- $user->loadCount('round_first');
- $user->loadCount('round_second');
- $user->loadCount('round_third');
- $user->loadCount('tournament_first');
- $user->loadCount('tournament_second');
- $user->loadCount('tournament_third');
- return $user->toJson();
+ $user->load([
+ 'participation',
+ 'participation.tournament',
+ ]);
+ $user->loadCount([
+ 'round_first',
+ 'round_second',
+ 'round_third',
+ 'tournament_first',
+ 'tournament_second',
+ 'tournament_third',
+ ]);
+
+ $json = $user->toArray();
+
+ if (!empty($validatedData['with'])) {
+ if (in_array('episodes_as_crew', $validatedData['with'])) {
+ $json['episodes_as_crew'] = $user->episodes_as_crew()->with([
+ 'channels',
+ 'crew' => function ($query) use ($request) {
+ $query->where('confirmed', true);
+ $query->orWhere('user_id', '=', $request->user()->id);
+ $query->orWhereIn('channel_id', $request->user()->channel_crews->pluck('channel_id'));
+ },
+ 'crew.user',
+ 'event',
+ 'players',
+ 'players.user',
+ ])->orderBy('start', 'DESC')->limit(50)->get();
+ }
+ if (in_array('episodes_as_comms', $validatedData['with'])) {
+ $json['episodes_as_comms'] = $user->episodes_as_comms()->with([
+ 'channels',
+ 'crew' => function ($query) use ($request) {
+ $query->where('confirmed', true);
+ $query->orWhere('user_id', '=', $request->user()->id);
+ $query->orWhereIn('channel_id', $request->user()->channel_crews->pluck('channel_id'));
+ },
+ 'crew.user',
+ 'event',
+ 'players',
+ 'players.user',
+ ])->orderBy('start', 'DESC')->limit(50)->get();
+ }
+ if (in_array('episodes_as_setup', $validatedData['with'])) {
+ $json['episodes_as_setup'] = $user->episodes_as_setup()->with([
+ 'channels',
+ 'crew' => function ($query) use ($request) {
+ $query->where('confirmed', true);
+ $query->orWhere('user_id', '=', $request->user()->id);
+ $query->orWhereIn('channel_id', $request->user()->channel_crews->pluck('channel_id'));
+ },
+ 'crew.user',
+ 'event',
+ 'players',
+ 'players.user',
+ ])->orderBy('start', 'DESC')->limit(50)->get();
+ }
+ if (in_array('episodes_as_tracker', $validatedData['with'])) {
+ $json['episodes_as_tracker'] = $user->episodes_as_tracker()->with([
+ 'channels',
+ 'crew' => function ($query) use ($request) {
+ $query->where('confirmed', true);
+ $query->orWhere('user_id', '=', $request->user()->id);
+ $query->orWhereIn('channel_id', $request->user()->channel_crews->pluck('channel_id'));
+ },
+ 'crew.user',
+ 'event',
+ 'players',
+ 'players.user',
+ ])->orderBy('start', 'DESC')->limit(50)->get();
+ }
+ if (in_array('episodes_as_runner', $validatedData['with'])) {
+ $json['episodes_as_runner'] = $user->episodes_as_runner()->with([
+ 'channels',
+ 'crew' => function ($query) use ($request) {
+ $query->where('confirmed', true);
+ $query->orWhere('user_id', '=', $request->user()->id);
+ $query->orWhereIn('channel_id', $request->user()->channel_crews->pluck('channel_id'));
+ },
+ 'crew.user',
+ 'event',
+ 'players',
+ 'players.user',
+ ])->orderBy('start', 'DESC')->limit(50)->get();
+ }
+ }
+
+ return $json;
}
}
return $this->hasMany(ChannelCrew::class);
}
+ public function episodes_as_crew() {
+ return $this
+ ->belongsToMany(Episode::class, 'episode_crews')
+ ->wherePivot('episode_crews.confirmed', '=', 1)
+ ->where('episodes.confirmed', '=', 1);
+ }
+
+ public function episodes_as_comms() {
+ return $this->episodes_as_crew()->wherePivot('episode_crews.role', '=', 'commentary');
+ }
+
+ public function episodes_as_setup() {
+ return $this->episodes_as_crew()->wherePivot('episode_crews.role', '=', 'setup');
+ }
+
+ public function episodes_as_tracker() {
+ return $this->episodes_as_crew()->wherePivot('episode_crews.role', '=', 'tracking');
+ }
+
+ public function episodes_as_runner() {
+ return $this
+ ->belongsToMany(Episode::class, 'episode_players')
+ ->where('episodes.confirmed', '=', 1);
+ }
+
public function participation() {
return $this->hasMany(Participant::class);
}
+import { defineConfig } from 'eslint/config';
import js from '@eslint/js';
import react from 'eslint-plugin-react';
import globals from 'globals';
-export default [
- js.configs.recommended,
- react.configs.flat.recommended,
+export default defineConfig([
{
+ extends: [
+ js.configs.recommended,
+ react.configs.flat.recommended,
+ ],
+
+ files: ['resources/js/**/*.{js,jsx}'],
+
plugins: {
react,
},
},
},
},
-];
+]);
return user
? <>
<Nav className="ms-auto">
- <LinkContainer to={`/users/${user.id}`}>
+ <LinkContainer to={`/users/${user.username}`}>
<Nav.Link>
<img alt="" src={getAvatarUrl(user)} />
{user.username}
</LinkContainer>
</Nav>
<Button
- className="ms-2"
+ className="ms-2"
onClick={logout}
title={t('button.logout')}
variant="outline-secondary"
import Item from './Item';
-const List = ({ episodes, onAddRestream, onApply, onEditRestream }) => {
+const List = ({
+ compact = false,
+ episodes,
+ onAddRestream,
+ onApply,
+ onEditRestream,
+}) => {
const grouped = React.useMemo(() => episodes.reduce((groups, episode) => {
const day = moment(episode.start).format('YYYY-MM-DD');
return {
};
}, {}), [episodes]);
- return <div className="episodes-list">
+ const className = React.useMemo(() => {
+ const classNames = ['episodes-list'];
+ if (compact) {
+ classNames.push('compact');
+ }
+ return classNames.join(' ');
+ }, [compact]);
+
+ return <div className={className}>
{Object.entries(grouped).map(([day, group]) => <div key={day}>
- <h2 className="text-center episodes-group-heading">
- {moment(day).format('dddd, L')}
- </h2>
+ {compact ?
+ <h3 className="episodes-group-heading">
+ {moment(day).format('dddd, L')}
+ </h3>
+ :
+ <h2 className="text-center episodes-group-heading">
+ {moment(day).format('dddd, L')}
+ </h2>
+ }
{group.map(episode =>
<Item
episode={episode}
};
List.propTypes = {
+ compact: PropTypes.bool,
episodes: PropTypes.arrayOf(PropTypes.shape({
start: PropTypes.string,
})),
return <Button
className="user-box"
- onClick={() => navigate(`/users/${user.id}`)}
+ href={`/users/${user.username}`}
+ onClick={(e) => {
+ navigate(`/users/${user.username}`);
+ e.preventDefault();
+ e.stopPropagation();
+ }}
variant="link"
>
{content}
import PropTypes from 'prop-types';
import React from 'react';
-import { Alert, Button, Col, Container, Row } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { Alert, Button, ButtonGroup, Col, Container, Row } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
import Box from './Box';
import Records from './Records';
import EditNicknameButton from './EditNicknameButton';
import EditStreamLinkButton from './EditStreamLinkButton';
import Participation from './Participation';
+import EpisodesList from '../episodes/List';
import Icon from '../common/Icon';
-import i18n from '../../i18n';
-const Profile = ({ user }) => <Container>
- <h1>
- {user.nickname || user.username}
- {' '}
- <EditNicknameButton user={user} />
- </h1>
- {user.random_quote && user.random_quote.comment ?
- <Alert className="quote-alert" variant="dark">
- <blockquote className="blockquote mb-0">
- {user.random_quote.comment}
- </blockquote>
- </Alert>
- : null}
- <Row>
- <Col md={6} className="mb-5">
- <h2>{i18n.t('users.discordTag')}</h2>
- <Box discriminator user={user} />
- </Col>
- <Col md={6} className="mb-5">
- <h2>{i18n.t('users.streamLink')}</h2>
- <p>
- {user.stream_link ?
- <Button
- href={user.stream_link}
- target="_blank"
- variant="outline-twitch"
- >
- <Icon.STREAM />
- {' '}
- {user.stream_link}
- </Button>
- :
- i18n.t('users.noStream')
- }
- {' '}
- <EditStreamLinkButton user={user} />
- </p>
- </Col>
- <Col md={6} className="mb-5">
- <h2>{i18n.t('users.tournamentRecords')}</h2>
- <Records
- first={user.tournament_first_count}
- second={user.tournament_second_count}
- third={user.tournament_third_count}
- />
- </Col>
- <Col md={6} className="mb-5">
- <h2>{i18n.t('users.roundRecords')}</h2>
- <Records
- first={user.round_first_count}
- second={user.round_second_count}
- third={user.round_third_count}
- />
- </Col>
- <Col md={12} className="mb-5">
- <h2>{i18n.t('users.tournaments')}</h2>
- <Participation user={user} />
- </Col>
- </Row>
-</Container>;
+const Profile = ({ user }) => {
+ const [activeEpisodes, setActiveEpisodes] = React.useState('runner');
+
+ const { t } = useTranslation();
+
+ React.useEffect(() => {
+ if (!user.episodes_as_runner.length) {
+ if (user.episodes_as_comms.length) {
+ setActiveEpisodes('comms');
+ } else if (user.episodes_as_tracker.length) {
+ setActiveEpisodes('tracker');
+ } else if (user.episodes_as_setup.length) {
+ setActiveEpisodes('setup');
+ }
+ } else {
+ setActiveEpisodes('runner');
+ }
+ }, [user])
+
+ const participation = React.useMemo(() => {
+ let p = 0;
+ if (user.episodes_as_runner.length) ++p;
+ if (user.episodes_as_comms.length) ++p;
+ if (user.episodes_as_tracker.length) ++p;
+ if (user.episodes_as_setup.length) ++p;
+ return p;
+ }, [user]);
+
+
+ return <Container>
+ <h1>
+ {user.nickname || user.username}
+ {' '}
+ <EditNicknameButton user={user} />
+ </h1>
+ {user.random_quote && user.random_quote.comment ?
+ <Alert className="quote-alert" variant="dark">
+ <blockquote className="blockquote mb-0">
+ {user.random_quote.comment}
+ </blockquote>
+ </Alert>
+ : null}
+ <Row>
+ <Col md={6} className="mb-5">
+ <h2>{t('users.discordTag')}</h2>
+ <Box discriminator user={user} />
+ </Col>
+ <Col md={6} className="mb-5">
+ <h2>{t('users.streamLink')}</h2>
+ <p>
+ {user.stream_link ?
+ <Button
+ href={user.stream_link}
+ target="_blank"
+ variant="outline-twitch"
+ >
+ <Icon.STREAM />
+ {' '}
+ {user.stream_link}
+ </Button>
+ :
+ t('users.noStream')
+ }
+ {' '}
+ <EditStreamLinkButton user={user} />
+ </p>
+ </Col>
+ <Col md={6} className="mb-5">
+ <h2>{t('users.tournamentRecords')}</h2>
+ <Records
+ first={user.tournament_first_count}
+ second={user.tournament_second_count}
+ third={user.tournament_third_count}
+ />
+ </Col>
+ <Col md={6} className="mb-5">
+ <h2>{t('users.roundRecords')}</h2>
+ <Records
+ first={user.round_first_count}
+ second={user.round_second_count}
+ third={user.round_third_count}
+ />
+ </Col>
+ <Col md={12} className="mb-5">
+ <h2>{t('users.tournaments')}</h2>
+ <Participation user={user} />
+ </Col>
+ {participation > 0 ?
+ <Col md={12} className="mb-5">
+ <h2>{t('users.episodes')}</h2>
+ {participation > 1 ?
+ <ButtonGroup>
+ {user.episodes_as_runner.length ?
+ <Button
+ onClick={() => setActiveEpisodes('runner')}
+ variant={activeEpisodes === 'runner' ? 'secondary' : 'outline-secondary'}
+ >
+ {t('button.asRunner')}
+ </Button>
+ : null}
+ {user.episodes_as_comms.length ?
+ <Button
+ onClick={() => setActiveEpisodes('comms')}
+ variant={activeEpisodes === 'comms' ? 'secondary' : 'outline-secondary'}
+ >
+ {t('button.asComms')}
+ </Button>
+ : null}
+ {user.episodes_as_tracker.length ?
+ <Button
+ onClick={() => setActiveEpisodes('tracker')}
+ variant={activeEpisodes === 'tracker' ? 'secondary' : 'outline-secondary'}
+ >
+ {t('button.asTracker')}
+ </Button>
+ : null}
+ {user.episodes_as_setup.length ?
+ <Button
+ onClick={() => setActiveEpisodes('setup')}
+ variant={activeEpisodes === 'setup' ? 'secondary' : 'outline-secondary'}
+ >
+ {t('button.asSetup')}
+ </Button>
+ : null}
+ </ButtonGroup>
+ : null}
+ {activeEpisodes === 'runner' ?
+ <EpisodesList compact episodes={user.episodes_as_runner} />
+ : null}
+ {activeEpisodes === 'comms' ?
+ <EpisodesList compact episodes={user.episodes_as_comms} />
+ : null}
+ {activeEpisodes === 'tracker' ?
+ <EpisodesList compact episodes={user.episodes_as_tracker} />
+ : null}
+ {activeEpisodes === 'setup' ?
+ <EpisodesList compact episodes={user.episodes_as_setup} />
+ : null}
+ </Col>
+ : null}
+ </Row>
+ </Container>;
+};
Profile.propTypes = {
user: PropTypes.shape({
+ episodes_as_comms: PropTypes.arrayOf(PropTypes.shape({
+ })),
+ episodes_as_runner: PropTypes.arrayOf(PropTypes.shape({
+ })),
+ episodes_as_setup: PropTypes.arrayOf(PropTypes.shape({
+ })),
+ episodes_as_tracker: PropTypes.arrayOf(PropTypes.shape({
+ })),
nickname: PropTypes.string,
participation: PropTypes.arrayOf(PropTypes.shape({
})),
}),
};
-export default withTranslation()(Profile);
+export default Profile;
},
button: {
add: 'Hinzufügen',
+ asComms: 'Als Kommentator',
+ asCrew: 'Als Crew',
+ asRunner: 'Als Runner',
+ asSetup: 'Als Setup-Helper',
+ asTracker: 'Als Tracker',
back: 'Zurück',
browserSource: 'Browser Source',
cancel: 'Abbrechen',
discordTag: 'Discord Tag',
editNickname: 'Name bearbeiten',
editStreamLink: 'Stream Link bearbeiten',
+ episodes: 'Restreams',
nickname: 'Name',
noStream: 'Kein Stream gesetzt',
participationEmpty: 'Hat noch an keinen Turnieren teilgenommen.',
},
button: {
add: 'Add',
+ asComms: 'As Commentary',
+ asCrew: 'As Crew',
+ asRunner: 'As Runner',
+ asSetup: 'As Setup Helper',
+ asTracker: 'As Tracker',
back: 'Back',
browserSource: 'Browser source',
cancel: 'Cancel',
discordTag: 'Discord tag',
editNickname: 'Edit name',
editStreamLink: 'Edit stream link',
+ episodes: 'Restreams',
nickname: 'Name',
noStream: 'No stream set',
participationEmpty: 'Has not participated in any tourneys yet.',
setLoading(true);
const ctrl = new AbortController();
axios
- .get(`/api/users/${id}`, { signal: ctrl.signal })
+ .get(`/api/users/${id}`, {
+ params: {
+ with: [
+ 'episodes_as_comms',
+ 'episodes_as_runner',
+ 'episodes_as_setup',
+ 'episodes_as_tracker',
+ ],
+ },
+ signal: ctrl.signal,
+ })
.then(response => {
setError(null);
setLoading(false);
z-index: 1;
}
+.episodes-list.compact .episodes-group-heading {
+ padding-bottom: 1rem;
+ background: linear-gradient(180deg, $body-bg, $body-bg 75%, transparent);
+}
+
.episodes-item {
background-size: 6rem auto;
background-repeat: no-repeat;