├── .gitignore
├── api
├── Procfile
├── Dockerfile
├── .prettierrc
├── .sequelizerc
├── routes-api
│ ├── utils_routes.js
│ ├── index.js
│ ├── track_routes.js
│ ├── cup_routes.js
│ ├── auth_routes.js
│ ├── patch_note_routes.js
│ └── user_routes.js
├── .env.example
├── .eslintrc
├── routes-socket
│ ├── index.js
│ ├── race_routes.js
│ ├── podium_routes.js
│ ├── tournament_routes.js
│ ├── streamers_chart_routes.js
│ └── participation_routes.js
├── models
│ ├── managers_editors.js
│ ├── streamers_chart.js
│ ├── signup_token.js
│ ├── podium.js
│ ├── cup.js
│ ├── track.js
│ ├── patch_note.js
│ ├── participation.js
│ ├── race.js
│ ├── index.js
│ ├── tournament.js
│ └── user.js
├── migrations
│ ├── 20200721084804-participation-goal.js
│ ├── 20210408192246-participation-nb-points.js
│ ├── 20210503184336-race-disconnection.js
│ ├── 20200713122646-participations.js
│ ├── 20200713122933-signup-tokens.js
│ ├── 20200713122023-cups.js
│ ├── 20200713122236-tracks.js
│ ├── 20200713122509-podia.js
│ ├── 20200713122810-races.js
│ ├── 20210519195756-managers-editors.js
│ ├── 20210912092013-streamers-chart.js
│ ├── 20210130213152-patch-notes.js
│ ├── 20200713120924-users.js
│ └── 20200713121802-tournaments.js
├── passport
│ ├── index.js
│ └── twitch_strategy.js
├── config
│ └── config.js
├── controllers
│ ├── track_ctrl.js
│ ├── cup_ctrl.js
│ └── podium_ctrl.js
├── package.json
├── index.js
├── .gitignore
└── utils.js
├── web-client
├── Procfile
├── client
│ ├── src
│ │ ├── assets
│ │ │ ├── sass
│ │ │ │ ├── plugins
│ │ │ │ │ ├── _index.scss
│ │ │ │ │ └── obs
│ │ │ │ │ │ ├── _index.scss
│ │ │ │ │ │ └── points
│ │ │ │ │ │ └── _index.scss
│ │ │ │ ├── auth
│ │ │ │ │ └── _index.scss
│ │ │ │ ├── statistics
│ │ │ │ │ └── _index.scss
│ │ │ │ ├── footer
│ │ │ │ │ └── _index.scss
│ │ │ │ ├── charts.scss
│ │ │ │ ├── settings
│ │ │ │ │ └── _index.scss
│ │ │ │ ├── podiums
│ │ │ │ │ └── _index.scss
│ │ │ │ ├── races
│ │ │ │ │ └── _index.scss
│ │ │ │ ├── cups
│ │ │ │ │ └── _index.scss
│ │ │ │ ├── _variables.scss
│ │ │ │ ├── users
│ │ │ │ │ └── _index.scss
│ │ │ │ ├── loader
│ │ │ │ │ └── _index.scss
│ │ │ │ ├── theme.scss
│ │ │ │ └── tournaments
│ │ │ │ │ └── _index.scss
│ │ │ ├── images
│ │ │ │ └── poncefleur.png
│ │ │ └── icons
│ │ │ │ └── wifiSlash.svg
│ │ ├── redux
│ │ │ ├── types
│ │ │ │ ├── settings.js
│ │ │ │ ├── socket.js
│ │ │ │ ├── statistics.js
│ │ │ │ ├── ponce.js
│ │ │ │ ├── tracks.js
│ │ │ │ ├── tournaments.js
│ │ │ │ ├── auth.js
│ │ │ │ ├── useComparisons.js
│ │ │ │ ├── patchNotes.js
│ │ │ │ └── useStreamersChart.js
│ │ │ ├── actions
│ │ │ │ ├── statistics.js
│ │ │ │ ├── ponce.js
│ │ │ │ ├── socket.js
│ │ │ │ ├── settings.js
│ │ │ │ ├── useComparisons.js
│ │ │ │ ├── tracks.js
│ │ │ │ ├── tournaments.js
│ │ │ │ ├── useStreamersChart.js
│ │ │ │ ├── patchNotes.js
│ │ │ │ └── auth.js
│ │ │ ├── selectors
│ │ │ │ ├── patchNotes.js
│ │ │ │ ├── tracks.js
│ │ │ │ └── tournaments.js
│ │ │ ├── store.js
│ │ │ └── reducers
│ │ │ │ ├── socket.js
│ │ │ │ ├── statistics.js
│ │ │ │ ├── ponce.js
│ │ │ │ ├── settings.js
│ │ │ │ ├── tracks.js
│ │ │ │ ├── index.js
│ │ │ │ ├── patchNotes.js
│ │ │ │ ├── tournaments.js
│ │ │ │ ├── auth.js
│ │ │ │ ├── useComparisons.js
│ │ │ │ └── useStreamersChart.js
│ │ ├── utils
│ │ │ ├── history.js
│ │ │ ├── createContext.js
│ │ │ ├── mergeRefs.js
│ │ │ ├── request.js
│ │ │ └── style.js
│ │ ├── services
│ │ │ ├── ponce.js
│ │ │ ├── cups.js
│ │ │ ├── auth.js
│ │ │ ├── tracks.js
│ │ │ ├── users.js
│ │ │ ├── user.js
│ │ │ └── patchNotes.js
│ │ ├── components
│ │ │ ├── admin
│ │ │ │ ├── users
│ │ │ │ │ ├── UserSkeleton.js
│ │ │ │ │ ├── UsersSkeleton.js
│ │ │ │ │ ├── UsersFilter.js
│ │ │ │ │ └── User.js
│ │ │ │ ├── tracks
│ │ │ │ │ ├── TracksListItem.js
│ │ │ │ │ ├── AddTrackBtn.js
│ │ │ │ │ └── TracksWrapper.js
│ │ │ │ ├── cups
│ │ │ │ │ ├── CupsListItem.js
│ │ │ │ │ ├── AddCupBtn.js
│ │ │ │ │ ├── CupsSkeleton.js
│ │ │ │ │ └── AddCupForm.js
│ │ │ │ ├── patchNotes
│ │ │ │ │ ├── PatchNoteSkeleton.js
│ │ │ │ │ ├── PatchNoteListItem.js
│ │ │ │ │ ├── PatchNotesSkeleton.js
│ │ │ │ │ ├── PatchNote.js
│ │ │ │ │ ├── PatchNoteWrapper.js
│ │ │ │ │ ├── EditPatchNoteWrapper.js
│ │ │ │ │ ├── PatchNoteFormSkeleton.js
│ │ │ │ │ ├── AddPatchNoteForm.js
│ │ │ │ │ ├── PatchNotes.js
│ │ │ │ │ └── EditPatchNoteForm.js
│ │ │ │ ├── tournaments
│ │ │ │ │ ├── TournamentsListItem.js
│ │ │ │ │ ├── TournamentFormSkeleton.js
│ │ │ │ │ ├── TournamentsSkeleton.js
│ │ │ │ │ ├── TournamentWrapper.js
│ │ │ │ │ ├── Tournament.js
│ │ │ │ │ ├── EditTournamentWrapper.js
│ │ │ │ │ ├── EditTournamentForm.js
│ │ │ │ │ ├── AddTournamentForm.js
│ │ │ │ │ ├── TournamentsWrapper.js
│ │ │ │ │ └── TournamentSkeleton.js
│ │ │ │ ├── AdminHeader.js
│ │ │ │ └── participations
│ │ │ │ │ ├── ParticipationSkeleton.js
│ │ │ │ │ ├── AddRaceForm.js
│ │ │ │ │ ├── AddRaceBtn.js
│ │ │ │ │ └── EditRaceForm.js
│ │ │ ├── utils
│ │ │ │ ├── Loader.js
│ │ │ │ ├── Switch.js
│ │ │ │ ├── ScrollToTop.js
│ │ │ │ ├── Error.js
│ │ │ │ ├── Analytics.js
│ │ │ │ ├── ErrorBoundary.js
│ │ │ │ ├── AppCrashed.js
│ │ │ │ └── Tabs.js
│ │ │ ├── races
│ │ │ │ ├── PonceRaces.js
│ │ │ │ ├── RacesSkeleton.js
│ │ │ │ └── RacesListItem.js
│ │ │ ├── statistics
│ │ │ │ ├── ChartSkeleton.js
│ │ │ │ ├── PaginationSkeleton.js
│ │ │ │ ├── UserStatistics.js
│ │ │ │ ├── PonceStatistics.js
│ │ │ │ ├── PointsCharts.js
│ │ │ │ ├── ParticipantsStatistics.js
│ │ │ │ └── Pagination.js
│ │ │ ├── form
│ │ │ │ ├── Select.js
│ │ │ │ ├── Form.js
│ │ │ │ ├── Textarea.js
│ │ │ │ ├── TracksTypeahead.js
│ │ │ │ ├── Button.js
│ │ │ │ └── UsersTypeahead.js
│ │ │ ├── podiums
│ │ │ │ ├── PodiumSkeleton.js
│ │ │ │ ├── AddPlayerForm.js
│ │ │ │ ├── EditPlayerForm.js
│ │ │ │ ├── PodiumListItem.js
│ │ │ │ └── AddPlayerBtn.js
│ │ │ ├── auth
│ │ │ │ ├── PrivateRoute.js
│ │ │ │ ├── Signin.js
│ │ │ │ └── AdminRoute.js
│ │ │ ├── participations
│ │ │ │ ├── ParticipationSkeleton.js
│ │ │ │ ├── PonceParticipations.js
│ │ │ │ ├── ParticipationChartSkeleton.js
│ │ │ │ ├── ParticipationGoalForm.js
│ │ │ │ ├── ParticipationPointsForm.js
│ │ │ │ ├── ParticipationComparisonsChart.js
│ │ │ │ ├── ParticipationChartLegends.js
│ │ │ │ ├── EditParticipationForm.js
│ │ │ │ └── ParticipationStreamersChart.js
│ │ │ ├── user
│ │ │ │ └── UserWrapperSkeleton.js
│ │ │ ├── patchNotes
│ │ │ │ └── LatestPatchNote.js
│ │ │ └── tournaments
│ │ │ │ └── TournamentInfos.js
│ │ ├── index.js
│ │ └── hooks
│ │ │ └── useSideEffects.js
│ ├── .env.example
│ ├── public
│ │ ├── robots.txt
│ │ ├── favicon.png
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ ├── index.html
│ │ └── sitemap.xml
│ ├── .prettierrc
│ └── .eslintrc
├── Dockerfile
├── package.json
├── index.js
└── .gitignore
├── .husky
└── pre-commit
├── package.json
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/api/Procfile:
--------------------------------------------------------------------------------
1 | web: node index.js
--------------------------------------------------------------------------------
/web-client/Procfile:
--------------------------------------------------------------------------------
1 | web: npm run start
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/plugins/_index.scss:
--------------------------------------------------------------------------------
1 | @import "obs"
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/plugins/obs/_index.scss:
--------------------------------------------------------------------------------
1 | @import "points"
--------------------------------------------------------------------------------
/web-client/client/src/redux/types/settings.js:
--------------------------------------------------------------------------------
1 | export const SET_THEME = 'SET_THEME';
2 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/types/socket.js:
--------------------------------------------------------------------------------
1 | export const SET_SOCKET = 'SET_SOCKET';
2 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/types/statistics.js:
--------------------------------------------------------------------------------
1 | export const SET_MAX_ITEMS = 'SET_MAX_ITEMS';
2 |
--------------------------------------------------------------------------------
/api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:17.1.0
2 |
3 | WORKDIR /app
4 |
5 | COPY . /app
6 |
7 | RUN npm install
8 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/auth/_index.scss:
--------------------------------------------------------------------------------
1 | .signup__form {
2 | margin-top: 2.5rem;
3 | }
4 |
--------------------------------------------------------------------------------
/web-client/client/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=
2 |
3 | REACT_APP_TOURNAMENT_CODE=
4 |
5 | REACT_APP_ANALYTICS_ID=
--------------------------------------------------------------------------------
/web-client/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/api/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "tabWidth": 4,
5 | "useTabs": false
6 | }
7 |
--------------------------------------------------------------------------------
/api/.sequelizerc:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | 'config': path.resolve('config', 'config.js')
5 | }
--------------------------------------------------------------------------------
/web-client/client/src/redux/types/ponce.js:
--------------------------------------------------------------------------------
1 | export const SET_PONCE = 'SET_PONCE';
2 | export const SET_LOADING = 'SET_PONCE_LOADING';
3 |
--------------------------------------------------------------------------------
/web-client/client/src/utils/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history';
2 |
3 | export default createBrowserHistory();
4 |
--------------------------------------------------------------------------------
/web-client/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16
2 |
3 | WORKDIR /app
4 |
5 | COPY . /app
6 |
7 | RUN npm install
8 | RUN npm run heroku-postbuild
9 |
--------------------------------------------------------------------------------
/web-client/client/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "tabWidth": 4,
5 | "useTabs": false
6 | }
7 |
--------------------------------------------------------------------------------
/web-client/client/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ceezik/ponce-tournois-mario-kart/HEAD/web-client/client/public/favicon.png
--------------------------------------------------------------------------------
/web-client/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ceezik/ponce-tournois-mario-kart/HEAD/web-client/client/public/logo192.png
--------------------------------------------------------------------------------
/web-client/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ceezik/ponce-tournois-mario-kart/HEAD/web-client/client/public/logo512.png
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | cd ./api
5 | npm run lint
6 |
7 | cd ../web-client/client
8 | npm run lint
--------------------------------------------------------------------------------
/web-client/client/src/assets/images/poncefleur.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ceezik/ponce-tournois-mario-kart/HEAD/web-client/client/src/assets/images/poncefleur.png
--------------------------------------------------------------------------------
/web-client/client/src/redux/types/tracks.js:
--------------------------------------------------------------------------------
1 | export const SET_TRACKS = 'SET_TRACKS';
2 | export const SET_ERROR = 'SET_ERROR';
3 | export const ADD_TRACK = 'ADD_TRACK';
4 |
--------------------------------------------------------------------------------
/web-client/client/src/services/ponce.js:
--------------------------------------------------------------------------------
1 | import request from '../utils/request';
2 |
3 | export const getPonce = () => {
4 | return request.get('/ponce');
5 | };
6 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/types/tournaments.js:
--------------------------------------------------------------------------------
1 | export const SET_TOURNAMENTS_STATE = 'SET_TOURNAMENTS_STATE';
2 | export const ADD_TOURNAMENT = 'ADD_TOURNAMENT';
3 | export const EDIT_TOURNAMENT = 'EDIT_TOURNAMENT';
4 |
--------------------------------------------------------------------------------
/web-client/client/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["plugin:prettier/recommended"],
4 | "plugins": ["prettier"],
5 | "rules": {
6 | "prettier/prettier": "error"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/types/auth.js:
--------------------------------------------------------------------------------
1 | export const SET_USER = 'SET_USER';
2 | export const SET_LOADING = 'SET_USER_LOADING';
3 | export const ADD_EDITOR = 'ADD_EDITOR';
4 | export const REMOVE_EDITOR = 'REMOVE_EDITOR';
5 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/types/useComparisons.js:
--------------------------------------------------------------------------------
1 | export const SET_COMPARISONS = 'SET_COMPARISONS';
2 | export const SET_LOADING = 'SET_LOADING_COMPARISONS';
3 | export const ON_GET_PARTICIPATIONS = 'ON_GET_COMPARISONS_PARTICIPATIONS';
4 |
--------------------------------------------------------------------------------
/api/routes-api/utils_routes.js:
--------------------------------------------------------------------------------
1 | const user_ctrl = require('../controllers/user_ctrl');
2 |
3 | module.exports = [
4 | {
5 | url: '/ponce',
6 | method: 'get',
7 | func: user_ctrl.getPonce,
8 | },
9 | ];
10 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/users/UserSkeleton.js:
--------------------------------------------------------------------------------
1 | import Skeleton from 'react-loading-skeleton';
2 |
3 | function UserSkeleton() {
4 | return ;
5 | }
6 |
7 | export default UserSkeleton;
8 |
--------------------------------------------------------------------------------
/web-client/client/src/components/utils/Loader.js:
--------------------------------------------------------------------------------
1 | function Loader() {
2 | return (
3 |
6 | );
7 | }
8 |
9 | export default Loader;
10 |
--------------------------------------------------------------------------------
/web-client/client/src/services/cups.js:
--------------------------------------------------------------------------------
1 | import request from '../utils/request';
2 |
3 | export const getAll = () => {
4 | return request.get('/cups');
5 | };
6 |
7 | export const create = (cup) => {
8 | return request.post('/cups', cup);
9 | };
10 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/actions/statistics.js:
--------------------------------------------------------------------------------
1 | import { SET_MAX_ITEMS } from '../types/statistics';
2 |
3 | export const setMaxItems = (maxItems) => (dispatch) => {
4 | dispatch({
5 | type: SET_MAX_ITEMS,
6 | payload: maxItems,
7 | });
8 | };
9 |
--------------------------------------------------------------------------------
/web-client/client/src/services/auth.js:
--------------------------------------------------------------------------------
1 | import request from '../utils/request';
2 |
3 | export const signup = (user) => {
4 | return request.post('/signup', user);
5 | };
6 |
7 | export const getProfil = () => {
8 | return request.get('/user');
9 | };
10 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/selectors/patchNotes.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | export const getPatchNoteById = createSelector(
4 | (state) => state.patchNotes,
5 | (_, id) => +id,
6 | ({ patchNotes }, id) => _.find(patchNotes, { id })
7 | );
8 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/selectors/tracks.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import _ from 'lodash';
3 |
4 | export const getSortedTracks = createSelector(
5 | (state) => state.tracks,
6 | ({ tracks }) => _.sortBy(tracks, (t) => t.name.toLowerCase())
7 | );
8 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/types/patchNotes.js:
--------------------------------------------------------------------------------
1 | export const SET_LATEST_PATCH_NOTE = 'SET_LATEST_PATCH_NOTE';
2 | export const SET_PATCH_NOTES_STATE = 'SET_PATCH_NOTES_STATE';
3 | export const ADD_PATCH_NOTE = 'ADD_PATCH_NOTE';
4 | export const EDIT_PATCH_NOTE = 'EDIT_PATCH_NOTE';
5 |
--------------------------------------------------------------------------------
/web-client/client/src/services/tracks.js:
--------------------------------------------------------------------------------
1 | import request from '../utils/request';
2 |
3 | export const getAll = () => {
4 | return request.get('/tracks');
5 | };
6 |
7 | export const create = (track, cupId) => {
8 | return request.post(`/cups/${cupId}/tracks`, track);
9 | };
10 |
--------------------------------------------------------------------------------
/api/.env.example:
--------------------------------------------------------------------------------
1 | REDIS_URL=
2 |
3 | WEB_CONCURRENCY=
4 | PORT=
5 |
6 | DB_HOST=
7 | DB_PORT=
8 | DB_NAME=
9 | DB_USER=
10 | DB_PWD=
11 |
12 | PASSPORT_CALLBACK=
13 | TWITCH_CLIENT_ID=
14 | TWITCH_CLIENT_SECRET=
15 |
16 | SECRET=
17 |
18 | PONCE_TWITCH_ID=
19 |
20 | WEB_CLIENT_URL=
--------------------------------------------------------------------------------
/web-client/client/src/utils/createContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export function createContext() {
4 | const Context = React.createContext(undefined);
5 |
6 | const useContext = () => React.useContext(Context);
7 |
8 | return [Context.Provider, useContext, Context];
9 | }
10 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/plugins/obs/points/_index.scss:
--------------------------------------------------------------------------------
1 | .OBSPluginPoints {
2 | width: 100%;
3 | height: 100%;
4 | background-color: #00ff00;
5 | text-align: center;
6 | font-size: 36px;
7 | font-weight: bold;
8 | color: white;
9 | -webkit-text-stroke: 1.5px black;
10 | }
--------------------------------------------------------------------------------
/api/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "env": {
7 | "es6": true
8 | },
9 | "extends": ["plugin:prettier/recommended"],
10 | "plugins": ["prettier"],
11 | "rules": {
12 | "prettier/prettier": "error"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/api/routes-socket/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | module.exports = (io, socket, userId, isAdmin) => {
4 | fs.readdirSync(__dirname)
5 | .filter((filename) => filename !== 'index.js')
6 | .forEach((filename) => {
7 | require('./' + filename)(io, socket, userId, isAdmin);
8 | });
9 | };
10 |
--------------------------------------------------------------------------------
/web-client/client/src/components/utils/Switch.js:
--------------------------------------------------------------------------------
1 | function Switch({ on, setOn }) {
2 | return (
3 |
7 | );
8 | }
9 |
10 | export default Switch;
11 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import reducers from './reducers';
4 |
5 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
6 | export default createStore(reducers, composeEnhancers(applyMiddleware(thunk)));
7 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/statistics/_index.scss:
--------------------------------------------------------------------------------
1 | .app__container .statistics__title:nth-of-type(n + 2) {
2 | margin-top: 3rem;
3 | }
4 |
5 | .statistics__paginationWrapper {
6 | display: flex;
7 | align-items: center;
8 | }
9 |
10 | .statistics__pagination {
11 | margin: 0 0.5em;
12 | min-width: 5.5em;
13 | }
14 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tracks/TracksListItem.js:
--------------------------------------------------------------------------------
1 | import { Col } from 'react-grid-system';
2 |
3 | function TracksListItem({ track }) {
4 | return (
5 |
6 | {track.name}
7 |
8 | );
9 | }
10 |
11 | export default TracksListItem;
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ponce-tournois-mario-kart",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1",
7 | "prepare": "husky install"
8 | },
9 | "author": "Ceezik",
10 | "license": "MIT",
11 | "devDependencies": {
12 | "husky": "^7.0.0"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/web-client/client/src/components/utils/ScrollToTop.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 |
4 | export default function ScrollToTop() {
5 | const { pathname } = useLocation();
6 |
7 | useEffect(() => {
8 | window.scrollTo(0, 0);
9 | }, [pathname]);
10 |
11 | return null;
12 | }
13 |
--------------------------------------------------------------------------------
/web-client/client/src/utils/mergeRefs.js:
--------------------------------------------------------------------------------
1 | export default function mergeRefs(refs) {
2 | return (value) => {
3 | refs.forEach((ref) => {
4 | if (typeof ref === 'function') {
5 | ref(value);
6 | } else if (ref != null) {
7 | ref.current = value;
8 | }
9 | });
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/api/routes-api/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | module.exports = (app) => {
4 | fs.readdirSync(__dirname)
5 | .filter((filename) => filename !== 'index.js')
6 | .forEach((filename) => {
7 | require('./' + filename).forEach((r) => {
8 | app[r.method](r.url, r.func);
9 | });
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/api/models/managers_editors.js:
--------------------------------------------------------------------------------
1 | const { Model } = require('sequelize');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | class ManagersEditors extends Model {}
5 |
6 | ManagersEditors.init(
7 | {},
8 | {
9 | sequelize,
10 | timestamps: false,
11 | }
12 | );
13 |
14 | return ManagersEditors;
15 | };
16 |
--------------------------------------------------------------------------------
/web-client/client/src/components/utils/Error.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 |
3 | function Error({ message }) {
4 | return (
5 |
6 |
7 | {message}
8 |
9 |
10 | );
11 | }
12 |
13 | export default Error;
14 |
--------------------------------------------------------------------------------
/web-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web-client",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "engines": {
6 | "node": ">= 13.7.0",
7 | "npm": ">= 6.13.7"
8 | },
9 | "scripts": {
10 | "start": "node index.js",
11 | "heroku-postbuild": "cd client && npm install && npm run build"
12 | },
13 | "dependencies": {
14 | "express": "^4.17.1"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/api/models/streamers_chart.js:
--------------------------------------------------------------------------------
1 | const { Model } = require('sequelize');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | class Dashboard extends Model {}
5 |
6 | Dashboard.init(
7 | {},
8 | {
9 | sequelize,
10 | modelName: 'StreamersChart',
11 | timestamps: false,
12 | }
13 | );
14 |
15 | return Dashboard;
16 | };
17 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/reducers/socket.js:
--------------------------------------------------------------------------------
1 | import { SET_SOCKET } from '../types/socket';
2 |
3 | const initialState = {
4 | socket: null,
5 | };
6 |
7 | export default function (state = initialState, action) {
8 | switch (action.type) {
9 | case SET_SOCKET:
10 | return { socket: action.payload };
11 | default:
12 | return state;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/web-client/client/src/services/users.js:
--------------------------------------------------------------------------------
1 | import request from '../utils/request';
2 |
3 | export const getAll = (params) => {
4 | return request.get(`/users`, { params });
5 | };
6 |
7 | export const getByUsername = (username) => {
8 | return request.get(`/users/${username}`);
9 | };
10 |
11 | export const updateById = (user) => {
12 | return request.put(`/users/${user.id}`, user);
13 | };
14 |
--------------------------------------------------------------------------------
/web-client/client/src/services/user.js:
--------------------------------------------------------------------------------
1 | import request from '../utils/request';
2 |
3 | export const update = (username) => {
4 | return request.put('/user', username);
5 | };
6 |
7 | export const addEditor = (username) => {
8 | return request.post('/user/editors', username);
9 | };
10 |
11 | export const removeEditor = (editor) => {
12 | return request.delete(`/user/editors/${editor}`);
13 | };
14 |
--------------------------------------------------------------------------------
/web-client/client/src/components/races/PonceRaces.js:
--------------------------------------------------------------------------------
1 | import { Helmet } from 'react-helmet';
2 | import Races from './Races';
3 |
4 | function PonceRaces() {
5 | return (
6 | <>
7 |
8 | Circuits joués
9 |
10 |
11 |
12 | >
13 | );
14 | }
15 |
16 | export default PonceRaces;
17 |
--------------------------------------------------------------------------------
/web-client/client/src/components/statistics/ChartSkeleton.js:
--------------------------------------------------------------------------------
1 | import Skeleton from 'react-loading-skeleton';
2 |
3 | function ChartSkeleton() {
4 | return (
5 | <>
6 |
7 |
8 |
9 |
10 |
11 | >
12 | );
13 | }
14 |
15 | export default ChartSkeleton;
16 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/actions/ponce.js:
--------------------------------------------------------------------------------
1 | import { getPonce } from '../../services/ponce';
2 | import { SET_PONCE, SET_LOADING } from '../types/ponce';
3 |
4 | export const fetchPonce = () => (dispatch) => {
5 | getPonce()
6 | .then((res) => dispatch({ type: SET_PONCE, payload: res.data }))
7 | .catch(() => {})
8 | .finally(() => dispatch({ type: SET_LOADING, payload: false }));
9 | };
10 |
--------------------------------------------------------------------------------
/api/migrations/20200721084804-participation-goal.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: (queryInterface, Sequelize) => {
5 | return queryInterface.addColumn('participations', 'goal', {
6 | type: Sequelize.INTEGER,
7 | });
8 | },
9 |
10 | down: (queryInterface, Sequelize) => {
11 | return queryInterface.removeColumn('participations', 'goal');
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/api/migrations/20210408192246-participation-nb-points.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: (queryInterface, Sequelize) => {
5 | return queryInterface.addColumn('participations', 'nbPoints', {
6 | type: Sequelize.INTEGER,
7 | });
8 | },
9 |
10 | down: (queryInterface, Sequelize) => {
11 | return queryInterface.removeColumn('participations', 'nbPoints');
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/api/passport/index.js:
--------------------------------------------------------------------------------
1 | const passport = require('passport'),
2 | db = require('../models');
3 |
4 | passport.serializeUser((user, done) => {
5 | done(null, user.id);
6 | });
7 |
8 | passport.deserializeUser((id, done) => {
9 | db.User.findByPk(id)
10 | .then((user) => {
11 | done(null, user);
12 | })
13 | .catch((err) => done(err));
14 | });
15 |
16 | passport.use(require('./twitch_strategy'));
17 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/types/useStreamersChart.js:
--------------------------------------------------------------------------------
1 | export const SET_STREAMERS = 'SET_STREAMERS';
2 | export const SET_LOADING_STREAMERS = 'SET_LOADING_STREAMERS';
3 | export const SET_STREAMERS_COMPARISONS = 'SET_STREAMERS_COMPARISONS';
4 | export const SET_LOADING_COMPARISONS = 'SET_LOADING_COMPARISONS';
5 | export const ON_GET_PARTICIPATIONS = 'ON_GET_STREAMERS_CHART_PARTICIPATIONS';
6 | export const RESET_STATE = 'RESET_USE_STREAMERS_CHART_STATE';
7 |
--------------------------------------------------------------------------------
/web-client/client/src/components/statistics/PaginationSkeleton.js:
--------------------------------------------------------------------------------
1 | import { Col, Row } from 'react-grid-system';
2 | import Skeleton from 'react-loading-skeleton';
3 |
4 | function PaginationSkeleton() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | );
12 | }
13 |
14 | export default PaginationSkeleton;
15 |
--------------------------------------------------------------------------------
/web-client/index.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const express = require("express");
3 | const app = express();
4 |
5 | const appPath = path.join(__dirname, "client", "build");
6 | const port = process.env.PORT || 3000;
7 |
8 | app.use(express.static(appPath));
9 | app.get("*", (req, res) => {
10 | res.sendFile(path.join(appPath, "index.html"));
11 | });
12 |
13 | app.listen(port, () => {
14 | console.log("Server is up on port : ", port);
15 | });
16 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/reducers/statistics.js:
--------------------------------------------------------------------------------
1 | import { SET_MAX_ITEMS } from '../types/statistics';
2 |
3 | const initialState = {
4 | maxItems: 25,
5 | itemsPerPage: [10, 25, 50, 100],
6 | };
7 |
8 | export default function (state = initialState, action) {
9 | switch (action.type) {
10 | case SET_MAX_ITEMS:
11 | return { ...state, maxItems: action.payload };
12 | default:
13 | return state;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/selectors/tournaments.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import _ from 'lodash';
3 |
4 | export const getReversedTournaments = createSelector(
5 | (state) => state.tournaments,
6 | ({ tournaments }) => [...tournaments].reverse()
7 | );
8 |
9 | export const getTournamentById = createSelector(
10 | (state) => state.tournaments,
11 | (_, id) => +id,
12 | ({ tournaments }, id) => _.find(tournaments, { id })
13 | );
14 |
--------------------------------------------------------------------------------
/api/routes-api/track_routes.js:
--------------------------------------------------------------------------------
1 | const track_ctrl = require('../controllers/track_ctrl');
2 | const auth_ctrl = require('../controllers/auth_ctrl');
3 |
4 | module.exports = [
5 | {
6 | url: '/tracks',
7 | method: 'get',
8 | func: track_ctrl.getAll,
9 | },
10 |
11 | {
12 | url: '/cups/:cupId/tracks',
13 | method: 'post',
14 | func: [auth_ctrl.isAuthenticated, auth_ctrl.isAdmin, track_ctrl.create],
15 | },
16 | ];
17 |
--------------------------------------------------------------------------------
/api/migrations/20210503184336-race-disconnection.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: (queryInterface, Sequelize) => {
5 | return queryInterface.addColumn('races', 'disconnected', {
6 | type: Sequelize.BOOLEAN,
7 | allowNull: false,
8 | defaultValue: false,
9 | });
10 | },
11 |
12 | down: (queryInterface, Sequelize) => {
13 | return queryInterface.removeColumn('races', 'disconnected');
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/actions/socket.js:
--------------------------------------------------------------------------------
1 | import socketIo from 'socket.io-client';
2 | import { SET_SOCKET } from '../types/socket';
3 |
4 | export const setSocket = (user) => (dispatch) => {
5 | const url = user
6 | ? `${process.env.REACT_APP_API_URL}?userId=${user.id}&isAdmin=${user.isAdmin}`
7 | : process.env.REACT_APP_API_URL;
8 | dispatch({
9 | type: SET_SOCKET,
10 | payload: socketIo(url, { transports: ['websocket'] }),
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/web-client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | client/node_modules
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | coverage
11 |
12 | # production
13 | client/build
14 |
15 | # misc
16 | .DS_Store
17 | .env
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | .eslintcache
24 | *.log
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
--------------------------------------------------------------------------------
/web-client/client/src/components/form/Select.js:
--------------------------------------------------------------------------------
1 | import ReactSelect from 'react-select';
2 | import { useSelector } from 'react-redux';
3 | import { getSelectStyle } from '../../utils/style';
4 |
5 | function Select(props) {
6 | const { theme } = useSelector((state) => state.settings);
7 |
8 | return (
9 | getSelectStyle(defaultStyle, theme)}
12 | />
13 | );
14 | }
15 |
16 | export default Select;
17 |
--------------------------------------------------------------------------------
/web-client/client/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import Cookies from 'js-cookie';
3 |
4 | const request = axios.create({
5 | baseURL: process.env.REACT_APP_API_URL,
6 | });
7 |
8 | request.defaults.headers.post['Content-Type'] = 'application/json';
9 |
10 | request.interceptors.request.use((config) => {
11 | const token = Cookies.get('token');
12 | if (token) config.headers.Authorization = `Bearer ${token}`;
13 |
14 | return config;
15 | });
16 |
17 | export default request;
18 |
--------------------------------------------------------------------------------
/web-client/client/src/components/podiums/PodiumSkeleton.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 | import Skeleton from 'react-loading-skeleton';
3 |
4 | function PodiumSkeleton() {
5 | return (
6 |
7 | {[...Array(3)].map((i, index) => (
8 |
9 |
10 |
11 | ))}
12 |
13 | );
14 | }
15 |
16 | export default PodiumSkeleton;
17 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/reducers/ponce.js:
--------------------------------------------------------------------------------
1 | import { SET_LOADING, SET_PONCE } from '../types/ponce';
2 |
3 | const intitialState = {
4 | ponce: null,
5 | loading: true,
6 | };
7 |
8 | export default function (state = intitialState, action) {
9 | switch (action.type) {
10 | case SET_PONCE:
11 | return { ...state, ponce: action.payload };
12 | case SET_LOADING:
13 | return { ...state, loading: action.payload };
14 | default:
15 | return state;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/web-client/client/src/components/utils/Analytics.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import ReactGA from 'react-ga';
3 | import { useLocation } from 'react-router-dom';
4 |
5 | function Analytics() {
6 | const location = useLocation();
7 |
8 | useEffect(() => ReactGA.initialize(process.env.REACT_APP_ANALYTICS_ID), []);
9 |
10 | useEffect(() => {
11 | ReactGA.set({ page: location.pathname });
12 | ReactGA.pageview(location.pathname);
13 | }, [location]);
14 |
15 | return <>>;
16 | }
17 |
18 | export default Analytics;
19 |
--------------------------------------------------------------------------------
/api/migrations/20200713122646-participations.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: async (queryInterface, Sequelize) => {
5 | return queryInterface.createTable('participations', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true,
11 | },
12 | });
13 | },
14 |
15 | down: async (queryInterface, Sequelize) => {
16 | return queryInterface.dropTable('participations');
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/api/routes-api/cup_routes.js:
--------------------------------------------------------------------------------
1 | const cup_ctrl = require('../controllers/cup_ctrl');
2 | const auth_ctrl = require('../controllers/auth_ctrl');
3 |
4 | module.exports = [
5 | {
6 | url: '/cups',
7 | method: 'get',
8 | func: cup_ctrl.getAll,
9 | },
10 |
11 | {
12 | url: '/cups',
13 | method: 'post',
14 | func: [auth_ctrl.isAuthenticated, auth_ctrl.isAdmin, cup_ctrl.create],
15 | },
16 |
17 | {
18 | url: '/cups/:cupId',
19 | method: 'use',
20 | func: cup_ctrl.loadById,
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/footer/_index.scss:
--------------------------------------------------------------------------------
1 | footer {
2 | margin-top: 5rem;
3 | flex-shrink: 0;
4 | text-align: center;
5 | border-top: 1px solid var(--border-color);
6 | padding: 2rem;
7 | background-color: var(--main-background-color);
8 | }
9 |
10 | .footer__links {
11 | & a {
12 | margin: 0 1em;
13 | }
14 | & a:first-of-type {
15 | margin-left: 0;
16 | }
17 | & a:last-of-type {
18 | margin-right: 0;
19 | }
20 | }
21 |
22 | .footer__logo {
23 | width: 3rem;
24 | height: 3rem;
25 | }
26 |
--------------------------------------------------------------------------------
/web-client/client/src/index.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom';
2 | import { Provider } from 'react-redux';
3 | import moment from 'moment';
4 | import 'moment/locale/fr';
5 | import './style.css';
6 | import App from './App';
7 | import store from './redux/store';
8 | import { ErrorBoundary } from './components/utils/ErrorBoundary';
9 |
10 | moment.locale('fr');
11 |
12 | ReactDOM.render(
13 |
14 |
15 |
16 |
17 | ,
18 | document.getElementById('root')
19 | );
20 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/actions/settings.js:
--------------------------------------------------------------------------------
1 | import { SET_THEME } from '../types/settings';
2 |
3 | const getTheme = () => {
4 | let theme = localStorage.getItem('theme');
5 | if (theme) return theme;
6 | return window.matchMedia('(prefers-color-scheme: dark)').matches
7 | ? 'dark'
8 | : 'light';
9 | };
10 |
11 | export const fetchTheme = () => (dispatch) => {
12 | dispatch({ type: SET_THEME, payload: getTheme() });
13 | };
14 |
15 | export const switchTheme = (theme) => (dispatch) => {
16 | dispatch({ type: SET_THEME, payload: theme });
17 | };
18 |
--------------------------------------------------------------------------------
/api/passport/twitch_strategy.js:
--------------------------------------------------------------------------------
1 | const TwitchStrategy = require('passport-twitch-new').Strategy,
2 | auth_ctrl = require('../controllers/auth_ctrl');
3 |
4 | module.exports = new TwitchStrategy(
5 | {
6 | clientID: process.env.TWITCH_CLIENT_ID,
7 | clientSecret: process.env.TWITCH_CLIENT_SECRET,
8 | callbackURL: `${process.env.PASSPORT_CALLBACK}/auth/twitch/callback`,
9 | scope: 'user_read',
10 | },
11 | (accessToken, refreshToken, profile, done) => {
12 | auth_ctrl.passportStrategy(profile.id, profile.login, done);
13 | }
14 | );
15 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/cups/CupsListItem.js:
--------------------------------------------------------------------------------
1 | import { Col } from 'react-grid-system';
2 |
3 | function CupsListItem({ cup, setSelectedCup, isSelected }) {
4 | return (
5 |
6 | setSelectedCup(cup)}
11 | >
12 | {cup.name}
13 |
14 |
15 | );
16 | }
17 |
18 | export default CupsListItem;
19 |
--------------------------------------------------------------------------------
/web-client/client/src/hooks/useSideEffects.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useSelector } from 'react-redux';
3 |
4 | export default ({ sideEffects, dependencies = [] }) => {
5 | const { socket } = useSelector((state) => state.socket);
6 |
7 | useEffect(() => {
8 | sideEffects.forEach((sideEffect) => {
9 | socket.on(sideEffect.event, sideEffect.callback);
10 | });
11 |
12 | return () =>
13 | sideEffects.forEach((sideEffect) => {
14 | socket.off(sideEffect.event);
15 | });
16 | }, dependencies);
17 | };
18 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/actions/useComparisons.js:
--------------------------------------------------------------------------------
1 | import {
2 | SET_COMPARISONS,
3 | SET_LOADING,
4 | ON_GET_PARTICIPATIONS,
5 | } from '../types/useComparisons';
6 |
7 | export const onGetParticipations = (participations) => (dispatch) => {
8 | dispatch({ type: ON_GET_PARTICIPATIONS, payload: participations });
9 | };
10 |
11 | export const setComparisons = (comparisons) => (dispatch) => {
12 | dispatch({ type: SET_COMPARISONS, payload: comparisons });
13 | };
14 |
15 | export const setLoading = (loading) => (dispatch) => {
16 | dispatch({ type: SET_LOADING, payload: loading });
17 | };
18 |
--------------------------------------------------------------------------------
/web-client/client/src/services/patchNotes.js:
--------------------------------------------------------------------------------
1 | import request from '../utils/request';
2 |
3 | export const getAll = () => {
4 | return request.get('/patch-notes');
5 | };
6 |
7 | export const getLatest = () => {
8 | return request.get('/patch-notes/latest');
9 | };
10 |
11 | export const create = (patchNote) => {
12 | return request.post('/patch-notes', patchNote);
13 | };
14 |
15 | export const getById = (id) => {
16 | return request.get(`/patch-notes/${id}`);
17 | };
18 |
19 | export const updateById = (id, patchNote) => {
20 | return request.put(`/patch-notes/${id}`, patchNote);
21 | };
22 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/actions/tracks.js:
--------------------------------------------------------------------------------
1 | import { getAll } from '../../services/tracks';
2 | import { ADD_TRACK, SET_TRACKS } from '../types/tracks';
3 |
4 | export const fetchTracks = () => (dispatch) => {
5 | getAll()
6 | .then((res) => dispatch({ type: SET_TRACKS, payload: res.data }))
7 | .catch(() =>
8 | dispatch({
9 | type: SET_TRACKS,
10 | payload: 'Impossible de récupérer les circuits',
11 | })
12 | );
13 | };
14 |
15 | export const addTrack = (track) => (dispatch) => {
16 | dispatch({ type: ADD_TRACK, payload: track });
17 | };
18 |
--------------------------------------------------------------------------------
/web-client/client/src/components/form/Form.js:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 | import { useForm } from 'react-hook-form';
3 |
4 | const FormContext = createContext();
5 | export const useFormContext = () => useContext(FormContext);
6 |
7 | function Form({ children, onSubmit, ...rest }) {
8 | const { handleSubmit, ...formValues } = useForm();
9 |
10 | return (
11 |
16 | );
17 | }
18 |
19 | export default Form;
20 |
--------------------------------------------------------------------------------
/api/routes-api/auth_routes.js:
--------------------------------------------------------------------------------
1 | const passport = require('passport'),
2 | auth_ctrl = require('../controllers/auth_ctrl');
3 |
4 | module.exports = [
5 | {
6 | url: '/auth/twitch',
7 | method: 'get',
8 | func: passport.authenticate('twitch'),
9 | },
10 |
11 | {
12 | url: '/auth/twitch/callback',
13 | method: 'get',
14 | func: [
15 | passport.authenticate('twitch', { session: false }),
16 | auth_ctrl.passportCallback,
17 | ],
18 | },
19 |
20 | {
21 | url: '/signup',
22 | method: 'post',
23 | func: auth_ctrl.signup,
24 | },
25 | ];
26 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/charts.scss:
--------------------------------------------------------------------------------
1 | .apexcharts-tooltip {
2 | box-shadow: none !important;
3 | background-color: var(--secondary-background-color) !important;
4 | border: 1px solid var(--border-color) !important;
5 | text-align: center !important;
6 | font-family: 'Nunito', sans-serif !important;
7 | font-weight: 700 !important;
8 | }
9 |
10 | .apexcharts-tooltip-title {
11 | background-color: var(--secondary-background-color) !important;
12 | border-bottom: 1px solid var(--border-color) !important;
13 | }
14 |
15 | /* https://github.com/apexcharts/apexcharts.js/issues/1620 */
16 | .apexcharts-tooltip-title:empty {
17 | display: none;
18 | }
19 |
--------------------------------------------------------------------------------
/web-client/client/src/components/auth/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { Route } from 'react-router-dom';
3 |
4 | const PrivateRoute = ({ component: Component, ...rest }) => {
5 | const { user } = useSelector((state) => state.auth);
6 |
7 | return (
8 |
11 | user ? :
12 | }
13 | />
14 | );
15 | };
16 |
17 | const RedirectToSignin = () => {
18 | window.location = `${process.env.REACT_APP_API_URL}/auth/twitch`;
19 | return <>>;
20 | };
21 |
22 | export default PrivateRoute;
23 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/reducers/settings.js:
--------------------------------------------------------------------------------
1 | import { SET_THEME } from '../types/settings';
2 |
3 | const intitialState = {
4 | theme: 'light',
5 | };
6 |
7 | export default function (state = intitialState, action) {
8 | switch (action.type) {
9 | case SET_THEME:
10 | if (action.payload === 'dark')
11 | document.documentElement.setAttribute('data-theme', 'dark');
12 | else document.documentElement.removeAttribute('data-theme');
13 | localStorage.setItem('theme', action.payload);
14 |
15 | return { ...state, theme: action.payload };
16 | default:
17 | return state;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/cups/AddCupBtn.js:
--------------------------------------------------------------------------------
1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
2 | import { faPlus } from '@fortawesome/free-solid-svg-icons';
3 | import { Col } from 'react-grid-system';
4 |
5 | function AddCupBtn({ setCreating }) {
6 | return (
7 | setCreating(true)}>
8 |
9 |
13 | Ajouter
14 |
15 |
16 | );
17 | }
18 |
19 | export default AddCupBtn;
20 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tracks/AddTrackBtn.js:
--------------------------------------------------------------------------------
1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
2 | import { faPlus } from '@fortawesome/free-solid-svg-icons';
3 | import { Col } from 'react-grid-system';
4 |
5 | function AddTrackBtn({ setCreating }) {
6 | return (
7 | setCreating(true)}>
8 |
9 |
13 | Ajouter
14 |
15 |
16 | );
17 | }
18 |
19 | export default AddTrackBtn;
20 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/cups/CupsSkeleton.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 | import Skeleton from 'react-loading-skeleton';
3 |
4 | function CupsSkeleton() {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 |
12 | {[...Array(12)].map((i, index) => (
13 |
14 |
15 |
16 | ))}
17 |
18 | >
19 | );
20 | }
21 |
22 | export default CupsSkeleton;
23 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/reducers/tracks.js:
--------------------------------------------------------------------------------
1 | import { SET_TRACKS, SET_ERROR, ADD_TRACK } from '../types/tracks';
2 |
3 | const initialState = {
4 | tracks: [],
5 | loading: true,
6 | error: null,
7 | };
8 |
9 | export default function (state = initialState, action) {
10 | switch (action.type) {
11 | case SET_TRACKS:
12 | return { tracks: action.payload, loading: false, error: null };
13 | case SET_ERROR:
14 | return { tracks: [], loading: false, error: action.payload };
15 | case ADD_TRACK:
16 | return { ...state, tracks: [...state.tracks, action.payload] };
17 | default:
18 | return state;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/api/migrations/20200713122933-signup-tokens.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: async (queryInterface, Sequelize) => {
5 | return queryInterface.createTable('signuptokens', {
6 | id: {
7 | type: Sequelize.UUID,
8 | defaultValue: Sequelize.UUIDV4,
9 | allowNull: false,
10 | primaryKey: true,
11 | },
12 | twitchId: {
13 | type: Sequelize.STRING,
14 | allowNull: false,
15 | },
16 | });
17 | },
18 |
19 | down: async (queryInterface, Sequelize) => {
20 | return queryInterface.dropTable('signuptokens');
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/web-client/client/src/components/auth/Signin.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { useHistory, useLocation } from 'react-router-dom';
4 | import queryString from 'query-string';
5 | import { signin } from '../../redux/actions/auth';
6 |
7 | function Signin() {
8 | const dispatch = useDispatch();
9 | const history = useHistory();
10 | const { search } = useLocation();
11 |
12 | const onSignin = () => history.push('/');
13 |
14 | useEffect(() => {
15 | const { token } = queryString.parse(search);
16 | dispatch(signin(token, onSignin));
17 | }, []);
18 |
19 | return <>>;
20 | }
21 |
22 | export default Signin;
23 |
--------------------------------------------------------------------------------
/web-client/client/src/components/participations/ParticipationSkeleton.js:
--------------------------------------------------------------------------------
1 | import TournamentSkeleton from '../admin/tournaments/TournamentSkeleton';
2 | import AdminParticipationSkeleton from '../admin/participations/ParticipationSkeleton';
3 | import { Row, Col } from 'react-grid-system';
4 |
5 | function ParticipationSkeleton({ showButton = true }) {
6 | return (
7 | <>
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | >
16 | );
17 | }
18 |
19 | export default ParticipationSkeleton;
20 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/users/UsersSkeleton.js:
--------------------------------------------------------------------------------
1 | import Skeleton from 'react-loading-skeleton';
2 | import { Row, Col } from 'react-grid-system';
3 | import UserSkeleton from './UserSkeleton';
4 |
5 | function UsersSkeleton() {
6 | return (
7 | <>
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {[...Array(3)].map((i, index) => (
18 |
19 | ))}
20 | >
21 | );
22 | }
23 |
24 | export default UsersSkeleton;
25 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import tracks from './tracks';
3 | import auth from './auth';
4 | import socket from './socket';
5 | import tournaments from './tournaments';
6 | import statistics from './statistics';
7 | import patchNotes from './patchNotes';
8 | import settings from './settings';
9 | import ponce from './ponce';
10 | import useStreamersChart from './useStreamersChart';
11 | import useComparisons from './useComparisons';
12 |
13 | export default combineReducers({
14 | tracks,
15 | auth,
16 | socket,
17 | tournaments,
18 | statistics,
19 | patchNotes,
20 | settings,
21 | ponce,
22 | useStreamersChart,
23 | useComparisons,
24 | });
25 |
--------------------------------------------------------------------------------
/web-client/client/src/components/statistics/UserStatistics.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 | import ParticipationsStatistics from './ParticipationsStatistics';
3 | import Pagination from './Pagination';
4 |
5 | function UserStatistics({ userId }) {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default UserStatistics;
23 |
--------------------------------------------------------------------------------
/api/models/signup_token.js:
--------------------------------------------------------------------------------
1 | const { Model } = require('sequelize');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | class SignupToken extends Model {}
5 |
6 | SignupToken.init(
7 | {
8 | id: {
9 | type: DataTypes.UUID,
10 | defaultValue: DataTypes.UUIDV4,
11 | primaryKey: true,
12 | allowNull: false,
13 | },
14 | twitchId: {
15 | type: DataTypes.STRING,
16 | allowNull: false,
17 | },
18 | },
19 | {
20 | sequelize,
21 | modelName: 'SignupToken',
22 | timestamps: false,
23 | }
24 | );
25 |
26 | return SignupToken;
27 | };
28 |
--------------------------------------------------------------------------------
/web-client/client/src/components/utils/ErrorBoundary.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AppCrashed from './AppCrashed';
3 |
4 | export class ErrorBoundary extends React.Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = { hasError: false };
8 | }
9 |
10 | static getDerivedStateFromError() {
11 | return { hasError: true };
12 | }
13 |
14 | componentDidCatch(error, errorInfo) {
15 | console.log(error, errorInfo);
16 | }
17 |
18 | render() {
19 | const { hasError } = this.state;
20 | const { children } = this.props;
21 | if (hasError) {
22 | return ;
23 | }
24 |
25 | return children;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/api/migrations/20200713122023-cups.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: async (queryInterface, Sequelize) => {
5 | return queryInterface.createTable('cups', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true,
11 | },
12 | name: {
13 | type: Sequelize.STRING,
14 | allowNull: false,
15 | unique: true,
16 | validate: { len: [3, 50] },
17 | },
18 | });
19 | },
20 |
21 | down: async (queryInterface, Sequelize) => {
22 | return queryInterface.dropTable('cups');
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/web-client/client/src/components/auth/AdminRoute.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { Route } from 'react-router-dom';
3 | import Error from '../utils/Error';
4 |
5 | const AdminRoute = ({ component: Component, ...rest }) => {
6 | const { user } = useSelector((state) => state.auth);
7 |
8 | return (
9 |
12 | user && user.isAdmin ? (
13 |
14 | ) : (
15 |
16 | )
17 | }
18 | />
19 | );
20 | };
21 |
22 | export default AdminRoute;
23 |
--------------------------------------------------------------------------------
/api/migrations/20200713122236-tracks.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: async (queryInterface, Sequelize) => {
5 | return queryInterface.createTable('tracks', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true,
11 | },
12 | name: {
13 | type: Sequelize.STRING,
14 | allowNull: false,
15 | unique: true,
16 | validate: { len: [3, 50] },
17 | },
18 | });
19 | },
20 |
21 | down: async (queryInterface, Sequelize) => {
22 | return queryInterface.dropTable('tracks');
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/api/config/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | development: {
3 | username: 'root',
4 | password: '',
5 | database: 'ponce-tournois-mario-kart',
6 | host: '127.0.0.1',
7 | port: 3306,
8 | dialect: 'mysql',
9 | },
10 | test: {
11 | username: 'root',
12 | password: '',
13 | database: 'ponce-tournois-mario-kart',
14 | host: '127.0.0.1',
15 | port: 3306,
16 | dialect: 'mysql',
17 | },
18 | production: {
19 | username: process.env.DB_USER,
20 | password: process.env.DB_PWD,
21 | database: process.env.DB_NAME,
22 | host: process.env.DB_HOST,
23 | port: process.env.DB_PORT,
24 | dialect: 'mysql',
25 | logging: false,
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/api/models/podium.js:
--------------------------------------------------------------------------------
1 | const { Model } = require('sequelize');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | class Podium extends Model {}
5 |
6 | Podium.init(
7 | {
8 | position: {
9 | type: DataTypes.INTEGER,
10 | allowNull: false,
11 | validate: { min: 1, max: 3 },
12 | },
13 | player: {
14 | type: DataTypes.STRING,
15 | allowNull: false,
16 | },
17 | },
18 | {
19 | sequelize,
20 | modelName: 'Podium',
21 | timestamps: false,
22 | }
23 | );
24 |
25 | Podium.associate = (db) => {
26 | Podium.belongsTo(db.Tournament);
27 | };
28 |
29 | return Podium;
30 | };
31 |
--------------------------------------------------------------------------------
/api/routes-socket/race_routes.js:
--------------------------------------------------------------------------------
1 | const race_ctrl = require('../controllers/race_ctrl');
2 |
3 | module.exports = (io, socket, userId, isAdmin) => {
4 | socket.on('getPonceRaces', (onError) => {
5 | race_ctrl.getPonceRaces(socket, onError);
6 | });
7 |
8 | socket.on('getUserRaces', (user, onError) => {
9 | race_ctrl.getUserRaces(socket, onError, user);
10 | });
11 |
12 | socket.on('addRace', (data, onError) => {
13 | race_ctrl.addRace(io, socket, onError, userId, data);
14 | });
15 |
16 | socket.on('editRace', (data, onError) => {
17 | race_ctrl.editRace(io, socket, onError, userId, data);
18 | });
19 |
20 | socket.on('deleteRace', (id, onError) => {
21 | race_ctrl.deleteRace(io, onError, userId, id);
22 | });
23 | };
24 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/users/UsersFilter.js:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from 'react';
2 | import _ from 'lodash';
3 |
4 | function UsersFilter({ usernameFilter, setUsernameFilter }) {
5 | const [username, setUsername] = useState(usernameFilter);
6 |
7 | const debounceFilter = useCallback(
8 | _.debounce((f) => setUsernameFilter(f), 300),
9 | []
10 | );
11 |
12 | return (
13 | {
18 | setUsername(e.target.value);
19 | debounceFilter(e.target.value);
20 | }}
21 | />
22 | );
23 | }
24 |
25 | export default UsersFilter;
26 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/patchNotes/PatchNoteSkeleton.js:
--------------------------------------------------------------------------------
1 | import { Col, Row } from 'react-grid-system';
2 | import Skeleton from 'react-loading-skeleton';
3 |
4 | function PatchNoteSkeleton() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | export default PatchNoteSkeleton;
25 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/settings/_index.scss:
--------------------------------------------------------------------------------
1 | .themeSwitch__label {
2 | margin-bottom: 0.5rem;
3 | }
4 |
5 | .settings__part {
6 | margin-bottom: 2rem;
7 | }
8 |
9 | .managersEditors__item {
10 | border: 1px solid var(--border-color);
11 | border-radius: 6px;
12 | padding: 1rem;
13 | margin: 0.5rem 0;
14 | }
15 |
16 | .managersEditors__delete {
17 | text-align: right;
18 | color: var(--error-color);
19 | cursor: pointer;
20 | }
21 |
22 | .managersEditors__addEditor {
23 | text-align: center;
24 | cursor: pointer;
25 | transition: box-shadow 0.3s ease;
26 | }
27 |
28 | .managersEditors__addEditor:hover {
29 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.15);
30 | }
31 |
32 | .managersEditors__addEditorBtn {
33 | color: var(--main-color);
34 | margin-right: 1rem;
35 | }
36 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/podiums/_index.scss:
--------------------------------------------------------------------------------
1 | .podium__player {
2 | padding: 1rem;
3 | margin: 0.5rem 0;
4 | border-radius: 6px;
5 | border: 1px solid var(--border-color);
6 | text-align: center;
7 | }
8 |
9 | .podium__player--skeleton {
10 | padding: 1rem 0;
11 | margin: 0.5rem 0;
12 | }
13 |
14 | .podium__playerTrophy {
15 | margin-right: 1rem;
16 | }
17 |
18 | .podium__addPlayer {
19 | cursor: pointer;
20 | transition: box-shadow 0.3s ease;
21 | }
22 |
23 | .podium__addPlayer:hover {
24 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.15);
25 | }
26 |
27 | .podium__addPlayerForm {
28 | text-align: start;
29 | }
30 |
31 | .podium__addPlayerWrapper {
32 | text-align: center;
33 | }
34 |
35 | .podium__addPlayerBtn {
36 | color: var(--main-color);
37 | margin-right: 1rem;
38 | }
39 |
--------------------------------------------------------------------------------
/api/routes-socket/podium_routes.js:
--------------------------------------------------------------------------------
1 | const podium_ctrl = require('../controllers/podium_ctrl');
2 |
3 | module.exports = (io, socket, userId, isAdmin) => {
4 | socket.on('getPodium', (tournamentId, onError) => {
5 | podium_ctrl.getPodium(socket, onError, tournamentId);
6 | });
7 |
8 | socket.on('addPodium', (podium, onError) => {
9 | if (isAdmin) {
10 | podium_ctrl.create(io, socket, onError, podium);
11 | } else {
12 | onError("Vous n'êtes pas autorisé à effectuer cette action");
13 | }
14 | });
15 |
16 | socket.on('editPodium', (podium, onError) => {
17 | if (isAdmin) {
18 | podium_ctrl.update(io, socket, onError, podium);
19 | } else {
20 | onError("Vous n'êtes pas autorisé à effectuer cette action");
21 | }
22 | });
23 | };
24 |
--------------------------------------------------------------------------------
/api/migrations/20200713122509-podia.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: async (queryInterface, Sequelize) => {
5 | return queryInterface.createTable('podia', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true,
11 | },
12 | position: {
13 | type: Sequelize.INTEGER,
14 | allowNull: false,
15 | validate: { min: 1, max: 3 },
16 | },
17 | player: {
18 | type: Sequelize.STRING,
19 | allowNull: false,
20 | },
21 | });
22 | },
23 |
24 | down: async (queryInterface, Sequelize) => {
25 | return queryInterface.dropTable('podia');
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/api/models/cup.js:
--------------------------------------------------------------------------------
1 | const { Model } = require('sequelize');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | class Cup extends Model {
5 | static nameIsUnique(name) {
6 | return this.findOne({ where: { name } })
7 | .then((c) => (c ? false : true))
8 | .catch((err) => false);
9 | }
10 | }
11 |
12 | Cup.init(
13 | {
14 | name: {
15 | type: DataTypes.STRING,
16 | allowNull: false,
17 | unique: true,
18 | validate: { len: [3, 50] },
19 | },
20 | },
21 | {
22 | sequelize,
23 | modelName: 'Cup',
24 | timestamps: false,
25 | }
26 | );
27 |
28 | Cup.associate = (db) => {
29 | Cup.hasMany(db.Track);
30 | };
31 |
32 | return Cup;
33 | };
34 |
--------------------------------------------------------------------------------
/web-client/client/src/components/participations/PonceParticipations.js:
--------------------------------------------------------------------------------
1 | import { Helmet } from 'react-helmet';
2 | import { useSelector } from 'react-redux';
3 | import { canUserManage } from '../../utils/utils';
4 | import Participations from './Participations';
5 |
6 | function PonceParticipations() {
7 | const { user } = useSelector((state) => state.auth);
8 | const { ponce } = useSelector((state) => state.ponce);
9 | const canManage = canUserManage(user, ponce?.id);
10 |
11 | return (
12 | <>
13 |
14 | Historique
15 |
16 |
17 |
22 | >
23 | );
24 | }
25 |
26 | export default PonceParticipations;
27 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/patchNotes/PatchNoteListItem.js:
--------------------------------------------------------------------------------
1 | import { Col } from 'react-grid-system';
2 | import { Link } from 'react-router-dom';
3 | import moment from 'moment';
4 |
5 | function PatchNoteListItem({ patchNote }) {
6 | return (
7 |
8 |
9 |
10 |
11 | {patchNote.version}
12 |
13 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default PatchNoteListItem;
23 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tournaments/TournamentsListItem.js:
--------------------------------------------------------------------------------
1 | import { Col } from 'react-grid-system';
2 | import { Link } from 'react-router-dom';
3 | import moment from 'moment';
4 |
5 | function TournamentsListItem({ tournament }) {
6 | return (
7 |
8 |
9 |
10 |
11 | {tournament.name}
12 |
13 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default TournamentsListItem;
23 |
--------------------------------------------------------------------------------
/web-client/client/src/components/statistics/PonceStatistics.js:
--------------------------------------------------------------------------------
1 | import { Helmet } from 'react-helmet';
2 | import { Row, Col } from 'react-grid-system';
3 | import ParticipantsStatistics from './ParticipantsStatistics';
4 | import ParticipationsStatistics from './ParticipationsStatistics';
5 | import Pagination from './Pagination';
6 |
7 | function PonceStatistics() {
8 | return (
9 |
10 |
11 | Statistiques
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | export default PonceStatistics;
27 |
--------------------------------------------------------------------------------
/api/migrations/20200713122810-races.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: async (queryInterface, Sequelize) => {
5 | return queryInterface.createTable('races', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true,
11 | },
12 | position: {
13 | type: Sequelize.INTEGER,
14 | allowNull: false,
15 | validate: { min: 1, max: 12 },
16 | },
17 | nbPoints: {
18 | type: Sequelize.INTEGER,
19 | allowNull: false,
20 | validate: { min: 1, max: 15 },
21 | },
22 | });
23 | },
24 |
25 | down: async (queryInterface, Sequelize) => {
26 | return queryInterface.dropTable('races');
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/api/routes-socket/tournament_routes.js:
--------------------------------------------------------------------------------
1 | const tournament_ctrl = require('../controllers/tournament_ctrl');
2 |
3 | module.exports = (io, socket, userId, isAdmin) => {
4 | socket.on('getTournaments', ({ page, pageSize }, onError) => {
5 | tournament_ctrl.getAll(socket, page, pageSize, onError);
6 | });
7 |
8 | socket.on('createTournament', (tournament, onError) => {
9 | if (isAdmin) {
10 | tournament_ctrl.create(io, socket, onError, tournament);
11 | } else {
12 | onError("Vous n'êtes pas autorisé à effectuer cette action");
13 | }
14 | });
15 |
16 | socket.on('updateTournament', (tournament, onError) => {
17 | if (isAdmin) {
18 | tournament_ctrl.updateById(io, socket, onError, tournament);
19 | } else {
20 | onError("Vous n'êtes pas autorisé à effectuer cette action");
21 | }
22 | });
23 | };
24 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/races/_index.scss:
--------------------------------------------------------------------------------
1 | .races__cup {
2 | margin: 3rem 0 1rem 0;
3 | }
4 |
5 | .races__wrapper .races__cup:first-child,
6 | .races__wrapper--skeleton > div:first-child > .races__cup {
7 | margin-top: 0;
8 | }
9 |
10 | .races__titleWrapper {
11 | padding: 0 1rem;
12 | }
13 |
14 | .races__title {
15 | color: var(--main-color);
16 | margin-bottom: 0.5rem;
17 | text-align: center;
18 | }
19 |
20 | .races__title > div:first-child {
21 | text-align: left;
22 | }
23 |
24 | .races__trackWrapper {
25 | border: 1px solid var(--border-color);
26 | border-radius: 6px;
27 | padding: 1rem;
28 | margin: 0.5rem 0;
29 | }
30 |
31 | .races__trackWrapper--skeleton {
32 | padding: 1rem 0rem;
33 | margin: 0.5rem 0;
34 | }
35 |
36 | .races__track {
37 | text-align: center;
38 | }
39 |
40 | .races__track > div:first-child {
41 | text-align: left;
42 | }
43 |
--------------------------------------------------------------------------------
/api/models/track.js:
--------------------------------------------------------------------------------
1 | const { Model } = require('sequelize');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | class Track extends Model {
5 | static nameIsUnique(name) {
6 | return this.findOne({ where: { name } })
7 | .then((c) => (c ? false : true))
8 | .catch((err) => false);
9 | }
10 | }
11 |
12 | Track.init(
13 | {
14 | name: {
15 | type: DataTypes.STRING,
16 | allowNull: false,
17 | unique: true,
18 | validate: { len: [3, 50] },
19 | },
20 | },
21 | {
22 | sequelize,
23 | modelName: 'Track',
24 | timestamps: false,
25 | }
26 | );
27 |
28 | Track.associate = (db) => {
29 | Track.belongsTo(db.Cup);
30 | Track.hasMany(db.Race);
31 | };
32 |
33 | return Track;
34 | };
35 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/actions/tournaments.js:
--------------------------------------------------------------------------------
1 | import {
2 | SET_TOURNAMENTS_STATE,
3 | ADD_TOURNAMENT,
4 | EDIT_TOURNAMENT,
5 | } from '../types/tournaments';
6 |
7 | export const setTournaments = (tournaments) => (dispatch) => {
8 | dispatch({
9 | type: SET_TOURNAMENTS_STATE,
10 | payload: { tournaments, loading: false, error: null },
11 | });
12 | };
13 |
14 | export const setTournamentsError = (error) => (dispatch) => {
15 | dispatch({
16 | type: SET_TOURNAMENTS_STATE,
17 | payload: { loading: false, error },
18 | });
19 | };
20 |
21 | export const addTournament = (tournament) => (dispatch) => {
22 | dispatch({
23 | type: ADD_TOURNAMENT,
24 | payload: tournament,
25 | });
26 | };
27 |
28 | export const editTournament = (tournament) => (dispatch) => {
29 | dispatch({
30 | type: EDIT_TOURNAMENT,
31 | payload: tournament,
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/web-client/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Tournoi des fleurs",
3 | "name": "Tournoi des fleurs",
4 | "description": "Suivez en temps réel les scores de Ponce et des fleurs dans le tournoi des fleurs, organisé chaque dimanche après-midi sur Mario Kart 8 Deluxe !",
5 | "lang": "fr",
6 | "orientation": "portrait-primary",
7 | "icons": [
8 | {
9 | "src": "favicon.png",
10 | "sizes": "64x64 32x32 24x24 16x16",
11 | "type": "image/png"
12 | },
13 | {
14 | "src": "logo192.png",
15 | "type": "image/png",
16 | "sizes": "192x192"
17 | },
18 | {
19 | "src": "logo512.png",
20 | "type": "image/png",
21 | "sizes": "512x512"
22 | }
23 | ],
24 | "start_url": ".",
25 | "display": "standalone",
26 | "theme_color": "#ff56a9",
27 | "background_color": "#ffffff"
28 | }
29 |
--------------------------------------------------------------------------------
/web-client/client/src/components/form/Textarea.js:
--------------------------------------------------------------------------------
1 | import { useFormContext } from './Form';
2 |
3 | function Textarea({
4 | children,
5 | name,
6 | validationSchema,
7 | label,
8 | className,
9 | ...rest
10 | }) {
11 | const { register, errors } = useFormContext();
12 |
13 | return (
14 |
15 | {label &&
}
16 |
24 | {children}
25 |
26 | {errors[name] && errors[name].message}
27 |
28 |
29 | );
30 | }
31 |
32 | export default Textarea;
33 |
--------------------------------------------------------------------------------
/api/migrations/20210519195756-managers-editors.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: (queryInterface, Sequelize) => {
5 | return queryInterface.createTable('managerseditors', {
6 | ManagerId: {
7 | type: Sequelize.INTEGER,
8 | references: {
9 | model: 'users',
10 | key: 'id',
11 | },
12 | allowNull: false,
13 | primaryKey: true,
14 | },
15 | EditorId: {
16 | type: Sequelize.INTEGER,
17 | references: {
18 | model: 'users',
19 | key: 'id',
20 | },
21 | allowNull: false,
22 | primaryKey: true,
23 | },
24 | });
25 | },
26 |
27 | down: (queryInterface, Sequelize) => {
28 | return queryInterface.dropTable('managerseditors');
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/cups/_index.scss:
--------------------------------------------------------------------------------
1 | .cupsList__track,
2 | .cupsList__cup {
3 | margin: 1rem 0;
4 | padding: 1rem 0;
5 | border-radius: 6px;
6 | text-align: center;
7 | border: 1px solid var(--border-color);
8 | transition: box-shadow 0.3s ease;
9 | }
10 |
11 | .cupsList__cup {
12 | text-transform: capitalize;
13 | }
14 |
15 | .cupsList__cup:hover {
16 | cursor: pointer;
17 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.15);
18 | }
19 |
20 | .cupsList__track--skeleton,
21 | .cupsList__cup--skeleton {
22 | margin: 1rem 0;
23 | padding: 1rem 0;
24 | }
25 |
26 | .cupsList__cup--selected {
27 | color: var(--main-color);
28 | border: 1px solid var(--main-color);
29 | }
30 |
31 | .cupsList__addCupIcon {
32 | color: var(--main-color);
33 | margin-right: 1rem;
34 | }
35 |
36 | .cupsList__addCupForm {
37 | margin-top: 2.5rem;
38 | }
39 |
40 | .cupsList__tracksWrapper {
41 | margin-top: 3rem;
42 | }
43 |
--------------------------------------------------------------------------------
/api/migrations/20210912092013-streamers-chart.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: (queryInterface, Sequelize) => {
5 | return queryInterface.createTable('streamerscharts', {
6 | TournamentId: {
7 | type: Sequelize.INTEGER,
8 | references: {
9 | model: 'tournaments',
10 | key: 'id',
11 | },
12 | allowNull: false,
13 | primaryKey: true,
14 | },
15 | StreamerId: {
16 | type: Sequelize.INTEGER,
17 | references: {
18 | model: 'users',
19 | key: 'id',
20 | },
21 | allowNull: false,
22 | primaryKey: true,
23 | },
24 | });
25 | },
26 |
27 | down: (queryInterface, Sequelize) => {
28 | return queryInterface.dropTable('streamerscharts');
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/api/models/patch_note.js:
--------------------------------------------------------------------------------
1 | const { Model } = require('sequelize');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | class PatchNote extends Model {
5 | static versionIsUnique(version, id = null) {
6 | return this.findOne({ where: { version } })
7 | .then((pn) => (pn ? pn.id === id : true))
8 | .catch((err) => false);
9 | }
10 | }
11 |
12 | PatchNote.init(
13 | {
14 | version: {
15 | type: DataTypes.STRING,
16 | allowNull: false,
17 | unique: true,
18 | validate: { len: [3, 100] },
19 | },
20 | content: {
21 | type: DataTypes.TEXT,
22 | allowNull: false,
23 | },
24 | },
25 | {
26 | sequelize,
27 | modelName: 'PatchNote',
28 | updatedAt: false,
29 | }
30 | );
31 |
32 | return PatchNote;
33 | };
34 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/_variables.scss:
--------------------------------------------------------------------------------
1 | /* Light theme */
2 | $light__main-background-color: white;
3 | $light__secondary-background-color: #f3f3f4;
4 |
5 | $light__border-color: #e6e6e6;
6 |
7 | $light__main-text-color: #1d1d1d;
8 | $light__tertiary-text-color: #989aa0;
9 |
10 | $light__main-color: #ff56a9;
11 | $light__main-color-dark: #ff469f;
12 | $light__main-color-light: #fda3cf;
13 |
14 | $light__error-color: #f3453f;
15 | $light__success-color: #68b684;
16 |
17 | $light__worst-chart-color: #ea4335;
18 |
19 | /* Dark theme */
20 | $dark__main-background-color: #36393f;
21 | $dark__secondary-background-color: #2f3136;
22 |
23 | $dark__border-color: #202225;
24 |
25 | $dark__main-text-color: white;
26 | $dark__tertiary-text-color: #989aa0;
27 |
28 | $dark__main-color: #ff56a9;
29 | $dark__main-color-dark: #ff469f;
30 | $dark__main-color-light: #fda3cf;
31 |
32 | $dark__error-color: #f3453f;
33 | $dark__success-color: #68b684;
34 |
35 | $dark__worst-chart-color: #ea4335;
36 |
--------------------------------------------------------------------------------
/web-client/client/src/components/user/UserWrapperSkeleton.js:
--------------------------------------------------------------------------------
1 | import { Col, Row } from 'react-grid-system';
2 | import Skeleton from 'react-loading-skeleton';
3 |
4 | function UserWrapperSkeleton() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {[...Array(4)].map((i, index) => (
14 |
20 |
21 |
22 | ))}
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default UserWrapperSkeleton;
30 |
--------------------------------------------------------------------------------
/api/migrations/20210130213152-patch-notes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: async (queryInterface, Sequelize) => {
5 | return queryInterface.createTable('patchnotes', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true,
11 | },
12 | version: {
13 | type: Sequelize.STRING,
14 | allowNull: false,
15 | unique: true,
16 | validate: { len: [3, 100] },
17 | },
18 | content: {
19 | type: Sequelize.TEXT,
20 | allowNull: false,
21 | },
22 | createdAt: {
23 | type: Sequelize.DATE,
24 | allowNull: false,
25 | },
26 | });
27 | },
28 |
29 | down: async (queryInterface, Sequelize) => {
30 | return queryInterface.dropTable('patchnotes');
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/web-client/client/src/components/form/TracksTypeahead.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { getSortedTracks } from '../../redux/selectors/tracks';
3 | import Typeahead from './Typeahead';
4 |
5 | function TracksTypeahead({ ...rest }) {
6 | const { loading, error } = useSelector((state) => state.tracks);
7 | const sortedTracks = useSelector(getSortedTracks);
8 |
9 | return (
10 | ({
14 | id: t.id,
15 | value: t.name,
16 | label: t.name,
17 | }))}
18 | loading={loading}
19 | error={error}
20 | messages={{
21 | loading: 'Récupération des circuits ...',
22 | error: 'Impossible de récupérer les circuits',
23 | noResult: 'Aucun circuit trouvé',
24 | }}
25 | />
26 | );
27 | }
28 |
29 | export default TracksTypeahead;
30 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/icons/wifiSlash.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/users/_index.scss:
--------------------------------------------------------------------------------
1 | .users__filter {
2 | margin-bottom: 1.5rem;
3 | }
4 |
5 | .users__titleWrapper {
6 | padding: 0 1rem;
7 | }
8 |
9 | .users__title {
10 | color: var(--main-color);
11 | margin-bottom: 0.5rem;
12 | text-align: center;
13 | }
14 |
15 | .users__title > div:first-child {
16 | text-align: left;
17 | }
18 |
19 | .users__userWrapper {
20 | border: 1px solid var(--border-color);
21 | border-radius: 6px;
22 | padding: 1rem;
23 | margin: 0.5rem 0;
24 | }
25 |
26 | .users__user--skeleton {
27 | padding: 1rem 0;
28 | margin: 0.5rem 0;
29 | }
30 |
31 | .users__user {
32 | text-align: center;
33 | }
34 |
35 | .users__user > div:first-child {
36 | text-align: left;
37 | }
38 |
39 | .userTitle {
40 | margin-top: 0;
41 | }
42 |
43 | .userMenu__wrapper {
44 | margin-bottom: 1.5em;
45 | }
46 |
47 | .userMenu__navListItem {
48 | padding: 0.25em 0;
49 | }
50 |
51 | .userMenu__navListItem--active {
52 | color: var(--main-color);
53 | }
54 |
--------------------------------------------------------------------------------
/api/models/participation.js:
--------------------------------------------------------------------------------
1 | const { Model } = require('sequelize');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | class Participation extends Model {}
5 |
6 | Participation.init(
7 | {
8 | goal: {
9 | type: DataTypes.NUMBER,
10 | },
11 | nbPoints: {
12 | type: DataTypes.NUMBER,
13 | },
14 | },
15 | {
16 | sequelize,
17 | modelName: 'Participation',
18 | timestamps: false,
19 | indexes: [{ unique: true, fields: ['TournamentId', 'UserId'] }],
20 | }
21 | );
22 |
23 | Participation.associate = (db) => {
24 | Participation.belongsTo(db.User);
25 | Participation.belongsTo(db.Tournament);
26 | Participation.hasMany(db.Race);
27 |
28 | Participation.addScope('defaultScope', {
29 | include: [{ model: db.Race }],
30 | });
31 | Participation.addScope('withoutRaces', {});
32 | };
33 |
34 | return Participation;
35 | };
36 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tournaments/TournamentFormSkeleton.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 | import Skeleton from 'react-loading-skeleton';
3 |
4 | function TournamentFormSkeleton() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 | {[...Array(5)].map((i, index) => (
13 |
14 |
15 |
16 |
17 | ))}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default TournamentFormSkeleton;
30 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tournaments/TournamentsSkeleton.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 | import Skeleton from 'react-loading-skeleton';
3 |
4 | function TournamentsSkeleton() {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | >
19 | );
20 | }
21 |
22 | export function TournamentsListSkeleton() {
23 | return (
24 |
25 | {[...Array(8)].map((i, index) => (
26 |
27 |
28 |
29 | ))}
30 |
31 | );
32 | }
33 |
34 | export default TournamentsSkeleton;
35 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/patchNotes/PatchNotesSkeleton.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 | import Skeleton from 'react-loading-skeleton';
3 |
4 | function PatchNotesSkeleton() {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | >
19 | );
20 | }
21 |
22 | export function PatchNotesListSkeleton() {
23 | return (
24 |
25 | {[...Array(8)].map((i, index) => (
26 |
27 |
28 |
29 | ))}
30 |
31 | );
32 | }
33 |
34 | export default PatchNotesSkeleton;
35 |
--------------------------------------------------------------------------------
/web-client/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
18 | Tournoi des fleurs
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/api/controllers/track_ctrl.js:
--------------------------------------------------------------------------------
1 | const db = require('../models');
2 |
3 | module.exports = {
4 | getAll: (req, res, next) => {
5 | return db.Track.findAll()
6 | .then((tracks) => res.json(tracks))
7 | .catch((err) => next(err));
8 | },
9 |
10 | create: (req, res, next) => {
11 | const { name } = req.body;
12 |
13 | if (name) {
14 | return db.Track.nameIsUnique(name)
15 | .then((isUnique) => {
16 | if (isUnique) {
17 | return req.cup
18 | .createTrack({ name })
19 | .then((track) => res.json(track))
20 | .catch((err) => next(err));
21 | }
22 | throw {
23 | status: 409,
24 | message: 'Un circuit avec ce nom existe déjà',
25 | };
26 | })
27 | .catch((err) => next(err));
28 | }
29 | throw { status: 406, message: 'Paramètres invalides' };
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ceezik
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/api/routes-api/patch_note_routes.js:
--------------------------------------------------------------------------------
1 | const patch_note_ctrl = require('../controllers/patch_note_ctrl');
2 | const auth_ctrl = require('../controllers/auth_ctrl');
3 |
4 | module.exports = [
5 | {
6 | url: '/patch-notes',
7 | method: 'get',
8 | func: patch_note_ctrl.getAll,
9 | },
10 |
11 | {
12 | url: '/patch-notes',
13 | method: 'post',
14 | func: [
15 | auth_ctrl.isAuthenticated,
16 | auth_ctrl.isAdmin,
17 | patch_note_ctrl.create,
18 | ],
19 | },
20 |
21 | {
22 | url: '/patch-notes/latest',
23 | method: 'get',
24 | func: patch_note_ctrl.getLatest,
25 | },
26 |
27 | {
28 | url: '/patch-notes/:patchNoteId',
29 | method: 'use',
30 | func: patch_note_ctrl.loadById,
31 | },
32 |
33 | {
34 | url: '/patch-notes/:patchNoteId',
35 | method: 'get',
36 | func: patch_note_ctrl.getById,
37 | },
38 |
39 | {
40 | url: '/patch-notes/:patchNoteId',
41 | method: 'put',
42 | func: patch_note_ctrl.updateById,
43 | },
44 | ];
45 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/actions/useStreamersChart.js:
--------------------------------------------------------------------------------
1 | import {
2 | SET_STREAMERS,
3 | SET_STREAMERS_COMPARISONS,
4 | SET_LOADING_STREAMERS,
5 | SET_LOADING_COMPARISONS,
6 | ON_GET_PARTICIPATIONS,
7 | RESET_STATE,
8 | } from '../types/useStreamersChart';
9 |
10 | export const onGetParticipations = (participations) => (dispatch) => {
11 | dispatch({ type: ON_GET_PARTICIPATIONS, payload: participations });
12 | };
13 |
14 | export const setStreamers = (streamers) => (dispatch) => {
15 | dispatch({ type: SET_STREAMERS, payload: streamers });
16 | };
17 |
18 | export const setLoadingStreamers = (loading) => (dispatch) => {
19 | dispatch({ type: SET_LOADING_STREAMERS, payload: loading });
20 | };
21 |
22 | export const setStreamersComparisons = (comparisons) => (dispatch) => {
23 | dispatch({ type: SET_STREAMERS_COMPARISONS, payload: comparisons });
24 | };
25 |
26 | export const setLoadingComparisons = (loading) => (dispatch) => {
27 | dispatch({ type: SET_LOADING_COMPARISONS, payload: loading });
28 | };
29 |
30 | export const resetState = () => (dispatch) => {
31 | dispatch({ type: RESET_STATE });
32 | };
33 |
--------------------------------------------------------------------------------
/api/models/race.js:
--------------------------------------------------------------------------------
1 | const { Model } = require('sequelize');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | class Race extends Model {}
5 |
6 | Race.init(
7 | {
8 | position: {
9 | type: DataTypes.INTEGER,
10 | allowNull: false,
11 | validate: { min: 1, max: 12 },
12 | },
13 | nbPoints: {
14 | type: DataTypes.INTEGER,
15 | allowNull: false,
16 | validate: { min: 1, max: 15 },
17 | },
18 | disconnected: {
19 | type: DataTypes.BOOLEAN,
20 | allowNull: false,
21 | defaultValue: false,
22 | },
23 | },
24 | {
25 | sequelize,
26 | modelName: 'Race',
27 | timestamps: false,
28 | }
29 | );
30 |
31 | Race.associate = (db) => {
32 | Race.belongsTo(db.Participation);
33 | Race.belongsTo(db.Track);
34 |
35 | Race.addScope('defaultScope', {
36 | include: [{ model: db.Track, attributes: ['name'] }],
37 | });
38 | };
39 |
40 | return Race;
41 | };
42 |
--------------------------------------------------------------------------------
/api/migrations/20200713120924-users.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: async (queryInterface, Sequelize) => {
5 | return queryInterface.createTable('users', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true,
11 | },
12 | username: {
13 | type: Sequelize.STRING,
14 | unique: true,
15 | allowNull: false,
16 | validate: { len: [3, 50] },
17 | },
18 | twitchId: {
19 | type: Sequelize.STRING,
20 | unique: true,
21 | allowNull: false,
22 | },
23 | isAdmin: {
24 | type: Sequelize.BOOLEAN,
25 | allowNull: false,
26 | defaultValue: false,
27 | },
28 | createdAt: {
29 | type: Sequelize.DATE,
30 | allowNull: false,
31 | },
32 | });
33 | },
34 |
35 | down: async (queryInterface, Sequelize) => {
36 | return queryInterface.dropTable('users');
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/web-client/client/src/components/participations/ParticipationChartSkeleton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Row, Col } from 'react-grid-system';
3 | import Skeleton from 'react-loading-skeleton';
4 |
5 | function ParticipationChartSkeleton({ showAddBtn = false }) {
6 | return (
7 | <>
8 |
9 | {[...Array(3)].map((i, index) => (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ))}
21 |
22 |
26 | {showAddBtn && }
27 | >
28 | );
29 | }
30 |
31 | export default ParticipationChartSkeleton;
32 |
--------------------------------------------------------------------------------
/web-client/client/src/components/podiums/AddPlayerForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import PlayerForm from './PlayerForm';
4 |
5 | function AddPlayerForm({ closeForm, tournamentId }) {
6 | const { socket } = useSelector((state) => state.socket);
7 | const [loading, setLoading] = useState(false);
8 | const [error, setError] = useState(null);
9 |
10 | useEffect(() => {
11 | socket.on('closeAddPlayerForm', () => closeForm());
12 |
13 | return () => socket.off('closeAddPlayerForm');
14 | }, []);
15 |
16 | const onSubmit = ({ player, position }) => {
17 | setLoading(true);
18 |
19 | socket.emit(
20 | 'addPodium',
21 | { player, position: parseInt(position), tournamentId },
22 | (err) => {
23 | setError(err);
24 | setLoading(false);
25 | }
26 | );
27 | };
28 |
29 | return (
30 |
36 | );
37 | }
38 |
39 | export default AddPlayerForm;
40 |
--------------------------------------------------------------------------------
/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "lint": "eslint .",
8 | "lint:fix": "eslint . --fix"
9 | },
10 | "author": "Ceezik",
11 | "license": "MIT",
12 | "engines": {
13 | "node": ">= 13.7.0",
14 | "npm": ">= 6.13.7"
15 | },
16 | "devDependencies": {
17 | "babel-eslint": "^10.1.0",
18 | "eslint": "^7.3.1",
19 | "eslint-config-prettier": "^8.3.0",
20 | "eslint-plugin-prettier": "^3.4.0",
21 | "prettier": "^2.0.5"
22 | },
23 | "dependencies": {
24 | "cors": "^2.8.5",
25 | "dotenv": "^8.2.0",
26 | "express": "^4.17.1",
27 | "express-jwt": "^6.0.0",
28 | "helmet": "^3.23.3",
29 | "jsonwebtoken": "^8.5.1",
30 | "lodash": "^4.17.19",
31 | "moment": "^2.27.0",
32 | "mysql2": "^2.1.0",
33 | "passport": "^0.4.1",
34 | "passport-twitch-new": "0.0.2",
35 | "sequelize": "^5.21.5",
36 | "sequelize-cli": "^6.2.0",
37 | "socket.io": "^2.3.0",
38 | "socket.io-redis": "^5.3.0"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/api/models/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs'),
2 | Sequelize = require('sequelize'),
3 | env = process.env.NODE_ENV || 'development',
4 | config = require(__dirname + '/../config/config')[env],
5 | db = {};
6 | let sequelize;
7 |
8 | if (config.use_env_variable) {
9 | sequelize = new Sequelize(process.env[config.use_env_variable], config);
10 | } else {
11 | sequelize = new Sequelize(
12 | config.database,
13 | config.username,
14 | config.password,
15 | config
16 | );
17 | }
18 |
19 | sequelize
20 | .authenticate()
21 | .then(() => {
22 | console.log('Connecté à la base de données');
23 | })
24 | .catch((err) => {
25 | console.error('Impossible de se connecter : ', err);
26 | });
27 |
28 | fs.readdirSync(__dirname)
29 | .filter((filename) => filename !== 'index.js')
30 | .forEach((filename) => {
31 | const model = sequelize.import('./' + filename);
32 | db[model.name] = model;
33 | });
34 |
35 | Object.keys(db).forEach((modelName) => {
36 | try {
37 | db[modelName].associate(db);
38 | } catch (e) {}
39 | });
40 |
41 | db.sequelize = sequelize;
42 | db.Sequelize = Sequelize;
43 |
44 | module.exports = db;
45 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/reducers/patchNotes.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_PATCH_NOTE,
3 | EDIT_PATCH_NOTE,
4 | SET_LATEST_PATCH_NOTE,
5 | SET_PATCH_NOTES_STATE,
6 | } from '../types/patchNotes';
7 |
8 | const intitialState = {
9 | latest: null,
10 | patchNotes: [],
11 | loading: true,
12 | error: null,
13 | };
14 |
15 | export default function (state = intitialState, action) {
16 | switch (action.type) {
17 | case SET_LATEST_PATCH_NOTE:
18 | return { ...state, latest: action.payload };
19 | case SET_PATCH_NOTES_STATE:
20 | return { ...state, ...action.payload };
21 | case ADD_PATCH_NOTE:
22 | return {
23 | ...state,
24 | patchNotes: [...state.patchNotes, action.payload],
25 | };
26 | case EDIT_PATCH_NOTE:
27 | return {
28 | ...state,
29 | patchNotes: state.patchNotes.map((patchNote) =>
30 | patchNote.id === action.payload.id
31 | ? { ...patchNote, ...action.payload }
32 | : patchNote
33 | ),
34 | };
35 | default:
36 | return state;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/web-client/client/src/components/participations/ParticipationGoalForm.js:
--------------------------------------------------------------------------------
1 | import Input from '../form/Input';
2 | import EditParticipationForm from './EditParticipationForm';
3 |
4 | function ParticipationGoalForm({ closeForm, nbMaxRaces, participation }) {
5 | const maxPoints = nbMaxRaces * 15;
6 | const GOAL_VALIDATION = `Veuillez entrer un nombre compris entre 1 et ${maxPoints}`;
7 |
8 | return (
9 |
13 |
30 |
31 | );
32 | }
33 |
34 | export default ParticipationGoalForm;
35 |
--------------------------------------------------------------------------------
/web-client/client/src/components/podiums/EditPlayerForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import PlayerForm from './PlayerForm';
4 |
5 | function EditPlayerForm({ closeForm, podium }) {
6 | const { socket } = useSelector((state) => state.socket);
7 | const [loading, setLoading] = useState(false);
8 | const [error, setError] = useState(null);
9 |
10 | useEffect(() => {
11 | socket.on('closeEditPlayerForm', () => closeForm());
12 |
13 | return () => socket.off('closeEditPlayerForm');
14 | }, []);
15 |
16 | const onSubmit = ({ player, position }) => {
17 | setLoading(true);
18 |
19 | socket.emit(
20 | 'editPodium',
21 | { player, position: parseInt(position), id: podium.id },
22 | (err) => {
23 | setError(err);
24 | setLoading(false);
25 | }
26 | );
27 | };
28 |
29 | return (
30 |
37 | );
38 | }
39 |
40 | export default EditPlayerForm;
41 |
--------------------------------------------------------------------------------
/api/routes-api/user_routes.js:
--------------------------------------------------------------------------------
1 | const user_ctrl = require('../controllers/user_ctrl');
2 | const auth_ctrl = require('../controllers/auth_ctrl');
3 |
4 | module.exports = [
5 | {
6 | url: '/users',
7 | method: 'get',
8 | func: user_ctrl.getAll,
9 | },
10 |
11 | {
12 | url: '/users/:username',
13 | method: 'get',
14 | func: user_ctrl.getByUsername,
15 | },
16 |
17 | {
18 | url: '/users/:userId',
19 | method: 'put',
20 | func: [
21 | auth_ctrl.isAuthenticated,
22 | auth_ctrl.isAdmin,
23 | user_ctrl.updateById,
24 | ],
25 | },
26 |
27 | {
28 | url: '/user',
29 | method: 'get',
30 | func: [auth_ctrl.isAuthenticated, user_ctrl.getCurrent],
31 | },
32 |
33 | {
34 | url: '/user',
35 | method: 'put',
36 | func: [auth_ctrl.isAuthenticated, user_ctrl.update],
37 | },
38 |
39 | {
40 | url: '/user/editors',
41 | method: 'post',
42 | func: [auth_ctrl.isAuthenticated, user_ctrl.addEditor],
43 | },
44 |
45 | {
46 | url: '/user/editors/:editorId',
47 | method: 'delete',
48 | func: [auth_ctrl.isAuthenticated, user_ctrl.removeEditor],
49 | },
50 | ];
51 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/patchNotes/PatchNote.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 | import { Helmet } from 'react-helmet';
3 | import { Link } from 'react-router-dom';
4 | import Markdown from 'react-markdown';
5 |
6 | function PatchNote({ patchNote }) {
7 | return (
8 |
9 |
10 | Patch note {patchNote.version}
11 |
12 |
13 |
14 |
15 |
16 |
20 | Modifier
21 |
22 |
23 |
24 |
25 |
26 | Patch note {patchNote.version}
27 |
28 |
29 |
30 | {patchNote.content}
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | export default PatchNote;
38 |
--------------------------------------------------------------------------------
/web-client/client/src/components/participations/ParticipationPointsForm.js:
--------------------------------------------------------------------------------
1 | import Input from '../form/Input';
2 | import EditParticipationForm from './EditParticipationForm';
3 |
4 | function ParticipationPointsForm({ closeForm, nbMaxRaces, participation }) {
5 | const maxPoints = nbMaxRaces * 15;
6 | const NB_POINTS_VALIDATION = `Veuillez entrer un nombre compris entre 1 et ${maxPoints}`;
7 |
8 | return (
9 |
13 |
30 |
31 | );
32 | }
33 |
34 | export default ParticipationPointsForm;
35 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/patchNotes/PatchNoteWrapper.js:
--------------------------------------------------------------------------------
1 | import { useParams } from 'react-router-dom';
2 | import { Row, Col } from 'react-grid-system';
3 | import { useSelector } from 'react-redux';
4 | import PatchNote from './PatchNote';
5 | import PatchNoteSkeleton from './PatchNoteSkeleton';
6 | import { getPatchNoteById } from '../../../redux/selectors/patchNotes';
7 |
8 | function PatchNoteWrapper() {
9 | const { patchNoteId } = useParams();
10 | const { loading } = useSelector((state) => state.patchNotes);
11 | const patchNote = useSelector((state) =>
12 | getPatchNoteById(state, patchNoteId)
13 | );
14 |
15 | return (
16 |
17 | {loading ? (
18 |
19 | ) : !patchNote ? (
20 |
21 |
22 |
23 | Ce patch note n'existe pas
24 |
25 |
26 |
27 | ) : (
28 |
29 | )}
30 |
31 | );
32 | }
33 |
34 | export default PatchNoteWrapper;
35 |
--------------------------------------------------------------------------------
/api/routes-socket/streamers_chart_routes.js:
--------------------------------------------------------------------------------
1 | const streamers_chart_ctrl = require('../controllers/streamers_chart_ctrl');
2 |
3 | module.exports = (io, socket, userId, isAdmin) => {
4 | socket.on('getStreamersChart', ({ tournament }, onError) => {
5 | streamers_chart_ctrl.getByTournament(socket, tournament, onError);
6 | });
7 |
8 | socket.on('addToStreamersChart', ({ tournament, username }, onError) => {
9 | if (isAdmin) {
10 | streamers_chart_ctrl.addToStreamersChart(
11 | io,
12 | socket,
13 | tournament,
14 | username,
15 | onError
16 | );
17 | } else {
18 | onError("Vous n'êtes pas autorisé à effectuer cette action");
19 | }
20 | });
21 |
22 | socket.on(
23 | 'removeFromStreamersChart',
24 | ({ tournament, username }, onError) => {
25 | if (isAdmin) {
26 | streamers_chart_ctrl.removeFromStreamersChart(
27 | io,
28 | tournament,
29 | username,
30 | onError
31 | );
32 | } else {
33 | onError("Vous n'êtes pas autorisé à effectuer cette action");
34 | }
35 | }
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/api/migrations/20200713121802-tournaments.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: async (queryInterface, Sequelize) => {
5 | return queryInterface.createTable('tournaments', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true,
11 | },
12 | name: {
13 | type: Sequelize.STRING,
14 | allowNull: false,
15 | unique: true,
16 | validate: { len: [3, 50] },
17 | },
18 | nbParticipants: {
19 | type: Sequelize.INTEGER,
20 | validate: { min: 1 },
21 | },
22 | nbMaxRaces: {
23 | type: Sequelize.INTEGER,
24 | allowNull: false,
25 | validate: { min: 1 },
26 | },
27 | startDate: {
28 | type: Sequelize.DATE,
29 | allowNull: false,
30 | },
31 | endDate: {
32 | type: Sequelize.DATE,
33 | allowNull: false,
34 | },
35 | });
36 | },
37 |
38 | down: async (queryInterface, Sequelize) => {
39 | return queryInterface.dropTable('tournaments');
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/loader/_index.scss:
--------------------------------------------------------------------------------
1 | .loader {
2 | overflow: hidden;
3 | width: 100%;
4 | height: 2px;
5 | background-color: transparent;
6 | margin: 0 auto;
7 | }
8 |
9 | .indeterminate {
10 | position: relative;
11 | width: 100%;
12 | height: 100%;
13 | }
14 |
15 | .indeterminate:before {
16 | content: '';
17 | position: absolute;
18 | height: 100%;
19 | background-color: white;
20 | animation: indeterminateFirst 1.5s infinite ease-out;
21 | }
22 |
23 | .indeterminate:after {
24 | content: '';
25 | position: absolute;
26 | height: 100%;
27 | background-color: #f1f1f1;
28 | animation: indeterminateSecond 1.5s infinite ease-in;
29 | }
30 |
31 | @keyframes indeterminateFirst {
32 | 0% {
33 | left: -100%;
34 | width: 100%;
35 | }
36 | 100% {
37 | left: 100%;
38 | width: 10%;
39 | }
40 | }
41 |
42 | @keyframes indeterminateSecond {
43 | 0% {
44 | left: -150%;
45 | width: 100%;
46 | }
47 | 100% {
48 | left: 100%;
49 | width: 10%;
50 | }
51 | }
52 |
53 | .btnSkeleton {
54 | padding: 0.75rem 2rem;
55 | }
56 |
57 | .selectSkeleton {
58 | padding: 0.75rem 0rem;
59 | }
60 |
61 | .inputSkeleton {
62 | margin-top: 0.5rem;
63 | padding: 0.75rem 1.5rem;
64 | }
65 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/patchNotes/EditPatchNoteWrapper.js:
--------------------------------------------------------------------------------
1 | import { useParams } from 'react-router-dom';
2 | import { Row, Col } from 'react-grid-system';
3 | import { useSelector } from 'react-redux';
4 | import EditPatchNoteForm from './EditPatchNoteForm';
5 | import PatchNoteFormSkeleton from './PatchNoteFormSkeleton';
6 | import { getPatchNoteById } from '../../../redux/selectors/patchNotes';
7 |
8 | function EditPatchNoteWrapper() {
9 | const { patchNoteId } = useParams();
10 | const { loading } = useSelector((state) => state.patchNotes);
11 | const patchNote = useSelector((state) =>
12 | getPatchNoteById(state, patchNoteId)
13 | );
14 |
15 | return (
16 |
17 | {loading ? (
18 |
19 | ) : !patchNote ? (
20 |
21 |
22 |
23 | Ce patch note n'existe pas
24 |
25 |
26 |
27 | ) : (
28 |
29 | )}
30 |
31 | );
32 | }
33 |
34 | export default EditPatchNoteWrapper;
35 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tournaments/TournamentWrapper.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 | import { useParams } from 'react-router-dom';
3 | import { useSelector } from 'react-redux';
4 | import Tournament from './Tournament';
5 | import TournamentSkeleton from './TournamentSkeleton';
6 | import { getTournamentById } from '../../../redux/selectors/tournaments';
7 |
8 | function TournamentWrapper() {
9 | const { tournamentId } = useParams();
10 | const { loading } = useSelector((state) => state.tournaments);
11 | const tournament = useSelector((state) =>
12 | getTournamentById(state, tournamentId)
13 | );
14 |
15 | return (
16 |
17 | {loading ? (
18 |
19 | ) : !tournament ? (
20 |
21 |
22 |
23 | Ce tournoi n'existe pas
24 |
25 |
26 |
27 | ) : (
28 |
29 | )}
30 |
31 | );
32 | }
33 |
34 | export default TournamentWrapper;
35 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/AdminHeader.js:
--------------------------------------------------------------------------------
1 | import { Row, Col, Container } from 'react-grid-system';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | function AdminHeader() {
5 | const LINKS = [
6 | { url: '/cups', name: 'Coupes/Circuits' },
7 | { url: '/tournaments', name: 'Tournois' },
8 | { url: '/users', name: 'Utilisateurs' },
9 | { url: '/patch-notes', name: 'Patch notes' },
10 | ];
11 |
12 | return (
13 |
14 |
15 |
16 | {LINKS.map((link, index) => (
17 |
22 |
26 | {link.name}
27 |
28 |
29 | ))}
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default AdminHeader;
37 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tournaments/Tournament.js:
--------------------------------------------------------------------------------
1 | import { Helmet } from 'react-helmet';
2 | import { Row, Col } from 'react-grid-system';
3 | import { Link } from 'react-router-dom';
4 | import PonceParticipation from '../participations/PonceParticipation';
5 | import TournamentInfos from '../../tournaments/TournamentInfos';
6 | import Podium from '../../podiums/Podium';
7 |
8 | function Tournament({ tournament }) {
9 | return (
10 |
11 |
12 | {tournament.name}
13 |
14 |
15 |
16 |
17 |
18 |
22 | Modifier
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | export default Tournament;
36 |
--------------------------------------------------------------------------------
/api/routes-socket/participation_routes.js:
--------------------------------------------------------------------------------
1 | const participation_ctrl = require('../controllers/participation_ctrl');
2 |
3 | module.exports = (io, socket, userId, isAdmin) => {
4 | socket.on('getPonceParticipations', (onError) => {
5 | participation_ctrl.getPonceParticipations(socket, onError);
6 | });
7 |
8 | socket.on('getUserParticipations', (user, onError) => {
9 | participation_ctrl.getUserParticipations(socket, onError, user);
10 | });
11 |
12 | socket.on('getPonceParticipation', (tournamentId, onError) => {
13 | participation_ctrl.getPonceByTournament(socket, onError, tournamentId);
14 | });
15 |
16 | socket.on('getLastPonceParticipation', (_, onError) => {
17 | participation_ctrl.getLastPonceParticipation(socket, onError);
18 | });
19 |
20 | socket.on('getLastUserParticipation', (user, onError) => {
21 | participation_ctrl.getLastUserParticipation(socket, onError, user);
22 | });
23 |
24 | socket.on('getParticipations', (participationsInfos, onError) => {
25 | participation_ctrl.getParticipations(
26 | socket,
27 | onError,
28 | participationsInfos
29 | );
30 | });
31 |
32 | socket.on('editParticipation', (participation, onError) => {
33 | participation_ctrl.update(io, socket, onError, participation, userId);
34 | });
35 | };
36 |
--------------------------------------------------------------------------------
/web-client/client/src/components/utils/AppCrashed.js:
--------------------------------------------------------------------------------
1 | import { Col, Container, Row } from 'react-grid-system';
2 | import Error from '../utils/Error';
3 |
4 | const ErrorBody = () => {
5 | const reloadPage = () => window.location.reload();
6 |
7 | return (
8 |
9 |
10 |
11 | Oups, une erreur est survenue ...
12 |
13 | Vous pouvez essayer de{' '}
14 | rafraichir la page
15 | , si le problème persiste veuillez me contacter sur{' '}
16 |
22 | Twitter
23 | {' '}
24 | ou Discord (Ceezik#3881)
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | function AppCrashed() {
33 | return } />;
34 | }
35 |
36 | export default AppCrashed;
37 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/reducers/tournaments.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import {
3 | SET_TOURNAMENTS_STATE,
4 | ADD_TOURNAMENT,
5 | EDIT_TOURNAMENT,
6 | } from '../types/tournaments';
7 |
8 | const initialState = {
9 | tournaments: [],
10 | loading: true,
11 | error: null,
12 | };
13 |
14 | export default function (state = initialState, action) {
15 | switch (action.type) {
16 | case SET_TOURNAMENTS_STATE:
17 | return { ...state, ...action.payload };
18 | case ADD_TOURNAMENT:
19 | return {
20 | ...state,
21 | tournaments: _.orderBy(
22 | [...state.tournaments, action.payload],
23 | ['startDate'],
24 | ['desc']
25 | ),
26 | };
27 | case EDIT_TOURNAMENT:
28 | return {
29 | ...state,
30 | tournaments: _.orderBy(
31 | state.tournaments.map((tournament) =>
32 | tournament.id === action.payload.id
33 | ? { ...tournament, ...action.payload }
34 | : tournament
35 | ),
36 | ['startDate'],
37 | ['desc']
38 | ),
39 | };
40 | default:
41 | return state;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/theme.scss:
--------------------------------------------------------------------------------
1 | @import 'variables';
2 |
3 | :root {
4 | --main-background-color: #{$light__main-background-color};
5 | --secondary-background-color: #{$light__secondary-background-color};
6 |
7 | --border-color: #{$light__border-color};
8 |
9 | --main-text-color: #{$light__main-text-color};
10 | --tertiary-text-color: #{$light__tertiary-text-color};
11 |
12 | --main-color: #{$light__main-color};
13 | --main-color-dark: #{$light__main-color-dark};
14 | --main-color-light: #{$light__main-color-light};
15 |
16 | --error-color: #{$light__error-color};
17 | --success-color: #{$light__success-color};
18 |
19 | --worst-chart-color: #{$light__worst-chart-color};
20 | }
21 |
22 | [data-theme='dark'] {
23 | --main-background-color: #{$dark__main-background-color};
24 | --secondary-background-color: #{$dark__secondary-background-color};
25 |
26 | --border-color: #{$dark__border-color};
27 |
28 | --main-text-color: #{$dark__main-text-color};
29 | --tertiary-text-color: #{$dark__tertiary-text-color};
30 |
31 | --main-color: #{$dark__main-color};
32 | --main-color-dark: #{$dark__main-color-dark};
33 | --main-color-light: #{$dark__main-color-light};
34 |
35 | --error-color: #{$dark__error-color};
36 | --success-color: #{$dark__success-color};
37 |
38 | --worst-chart-color: #{$dark__worst-chart-color};
39 | }
40 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/patchNotes/PatchNoteFormSkeleton.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 | import Skeleton from 'react-loading-skeleton';
3 |
4 | function PatchNoteFormSkeleton() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | export default PatchNoteFormSkeleton;
39 |
--------------------------------------------------------------------------------
/web-client/client/src/components/statistics/PointsCharts.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import ApexChart from 'react-apexcharts';
3 | import { getParticipationNbPoints } from '../../utils/utils';
4 | import Chart from '../utils/Chart';
5 |
6 | export function TotalPointsChart({ participations }) {
7 | const series = [
8 | {
9 | data: participations.map(
10 | (p) => getParticipationNbPoints(p) || null
11 | ),
12 | },
13 | ];
14 |
15 | return (
16 | p.Tournament.name)}
20 | />
21 | );
22 | }
23 |
24 | export function AveragePointsChart({ participations }) {
25 | const getAveragePoints = () => {
26 | return participations.map((p) => {
27 | if (p.nbPoints) return null;
28 |
29 | const nbRaces = p.Races.length;
30 | if (!nbRaces) return null;
31 |
32 | const nbPoints = _.sumBy(p.Races, 'nbPoints');
33 | return nbRaces ? (nbPoints / nbRaces).toFixed(1) : 0;
34 | });
35 | };
36 |
37 | const series = [{ data: getAveragePoints() }];
38 |
39 | return (
40 | p.Tournament.name)}
44 | />
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_EDITOR,
3 | REMOVE_EDITOR,
4 | SET_LOADING,
5 | SET_USER,
6 | } from '../types/auth';
7 |
8 | const intitialState = {
9 | user: null,
10 | loading: true,
11 | };
12 |
13 | export default function (state = intitialState, action) {
14 | switch (action.type) {
15 | case SET_USER:
16 | return { ...state, user: action.payload };
17 | case SET_LOADING:
18 | return { ...state, loading: action.payload };
19 | case ADD_EDITOR:
20 | if (
21 | !state.user ||
22 | state.user.Editors.find((e) => e.id === action.payload.id)
23 | )
24 | return state;
25 | return {
26 | ...state,
27 | user: {
28 | ...state.user,
29 | Editors: [...state.user.Editors, action.payload],
30 | },
31 | };
32 | case REMOVE_EDITOR:
33 | if (!state.user) return state;
34 | return {
35 | ...state,
36 | user: {
37 | ...state.user,
38 | Editors: state.user.Editors.filter(
39 | (e) => e.id !== action.payload
40 | ),
41 | },
42 | };
43 | default:
44 | return state;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/actions/patchNotes.js:
--------------------------------------------------------------------------------
1 | import { getAll, getLatest } from '../../services/patchNotes';
2 | import {
3 | ADD_PATCH_NOTE,
4 | EDIT_PATCH_NOTE,
5 | SET_LATEST_PATCH_NOTE,
6 | SET_PATCH_NOTES_STATE,
7 | } from '../types/patchNotes';
8 |
9 | export const fetchLatestPatchNote = () => (dispatch) => {
10 | getLatest()
11 | .then((res) => {
12 | dispatch({
13 | type: SET_LATEST_PATCH_NOTE,
14 | payload: res.data,
15 | });
16 | })
17 | .catch(() => {});
18 | };
19 |
20 | export const fetchPatchNotes = () => (dispatch) => {
21 | getAll()
22 | .then((res) =>
23 | dispatch({
24 | type: SET_PATCH_NOTES_STATE,
25 | payload: { patchNotes: res.data, loading: false, error: null },
26 | })
27 | )
28 | .catch((err) =>
29 | dispatch({
30 | type: SET_PATCH_NOTES_STATE,
31 | payload: { loading: false, error: err.response.data },
32 | })
33 | );
34 | };
35 |
36 | export const addPatchNote = (patchNote) => (dispatch) => {
37 | dispatch({
38 | type: ADD_PATCH_NOTE,
39 | payload: patchNote,
40 | });
41 | };
42 |
43 | export const editPatchNote = (patchNote) => (dispatch) => {
44 | dispatch({
45 | type: EDIT_PATCH_NOTE,
46 | payload: patchNote,
47 | });
48 | };
49 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/reducers/useComparisons.js:
--------------------------------------------------------------------------------
1 | import { getComparisonColor, isComparisonUnique } from '../../utils/utils';
2 | import {
3 | ON_GET_PARTICIPATIONS,
4 | SET_COMPARISONS,
5 | SET_LOADING,
6 | } from '../types/useComparisons';
7 |
8 | const intitialState = {
9 | comparisons: [],
10 | loading: true,
11 | };
12 |
13 | export default function (state = intitialState, action) {
14 | switch (action.type) {
15 | case ON_GET_PARTICIPATIONS:
16 | const { comparisons: newComparisons } = action.payload.reduce(
17 | (acc, curr) => {
18 | if (!isComparisonUnique(curr, state.comparisons))
19 | return acc;
20 |
21 | const color = getComparisonColor(acc.alreadyUsedColors);
22 | return {
23 | comparisons: [...acc.comparisons, { ...curr, color }],
24 | alreadyUsedColors: [...acc.alreadyUsedColors, color],
25 | };
26 | },
27 | { comparisons: [], alreadyUsedColors: [] }
28 | );
29 |
30 | return { ...state, comparisons: newComparisons, loading: false };
31 | case SET_COMPARISONS:
32 | return { ...state, comparisons: action.payload };
33 | case SET_LOADING:
34 | return { ...state, loading: action.payload };
35 | default:
36 | return state;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/api/controllers/cup_ctrl.js:
--------------------------------------------------------------------------------
1 | const db = require('../models');
2 |
3 | module.exports = {
4 | getAll: (req, res, next) => {
5 | return db.Cup.findAll({ order: [['id', 'ASC']] })
6 | .then((cups) => res.json(cups))
7 | .catch((err) => next(err));
8 | },
9 |
10 | create: (req, res, next) => {
11 | const { name } = req.body;
12 |
13 | if (name) {
14 | return db.Cup.nameIsUnique(name)
15 | .then((isUnique) => {
16 | if (isUnique) {
17 | return db.Cup.create({ name })
18 | .then((cup) => res.json(cup))
19 | .catch((err) => next(err));
20 | }
21 | throw {
22 | status: 409,
23 | message: 'Une coupe avec ce nom existe déjà',
24 | };
25 | })
26 | .catch((err) => next(err));
27 | }
28 | throw { status: 406, message: 'Paramètres invalides' };
29 | },
30 |
31 | loadById: (req, res, next) => {
32 | return db.Cup.findByPk(req.params.cupId)
33 | .then((cup) => {
34 | if (cup) {
35 | req.cup = cup;
36 | return next();
37 | }
38 | throw { status: 404, message: "Cette coupe n'existe pas" };
39 | })
40 | .catch((err) => next(err));
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/web-client/client/src/components/form/Button.js:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from 'react';
2 | import { useFormContext } from './Form';
3 | import Loader from '../utils/Loader';
4 |
5 | function Button({ children, loading, disabled, primary = true, ...props }) {
6 | const { errors } = useFormContext();
7 | const [width, setWidth] = useState(0);
8 | const [height, setHeight] = useState(0);
9 | const ref = useRef(null);
10 |
11 | useEffect(() => {
12 | if (ref.current && ref.current.getBoundingClientRect().width) {
13 | setWidth(ref.current.getBoundingClientRect().width);
14 | }
15 | if (ref.current && ref.current.getBoundingClientRect().height) {
16 | setHeight(ref.current.getBoundingClientRect().height);
17 | }
18 | }, [children]);
19 |
20 | return (
21 |
40 | );
41 | }
42 |
43 | export default Button;
44 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/users/User.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { Col, Row } from 'react-grid-system';
4 | import Switch from '../../utils/Switch';
5 | import { updateById } from '../../../services/users';
6 |
7 | function User({ user }) {
8 | const [isAdmin, setIsAdmin] = useState(user.isAdmin);
9 |
10 | const update = (isAdmin) => {
11 | setIsAdmin(isAdmin);
12 | updateById({ id: user.id, isAdmin }).catch(() => setIsAdmin(!isAdmin));
13 | };
14 |
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
25 | {user.username}
26 |
27 |
28 |
29 | update(!isAdmin)}
32 | />
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
41 | export default User;
42 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tournaments/EditTournamentWrapper.js:
--------------------------------------------------------------------------------
1 | import { useParams } from 'react-router-dom';
2 | import { useSelector } from 'react-redux';
3 | import { Row, Col } from 'react-grid-system';
4 | import EditTournamentForm from './EditTournamentForm';
5 | import TournamentFormSkeleton from './TournamentFormSkeleton';
6 | import { getTournamentById } from '../../../redux/selectors/tournaments';
7 |
8 | function EditTournamentWrapper() {
9 | const { tournamentId } = useParams();
10 | const { loading } = useSelector((state) => state.tournaments);
11 | const tournament = useSelector((state) =>
12 | getTournamentById(state, tournamentId)
13 | );
14 |
15 | return (
16 |
17 | {loading ? (
18 |
19 | ) : !tournament ? (
20 |
21 |
22 |
23 | Ce tournoi n'existe pas
24 |
25 |
26 |
27 | ) : (
28 |
29 |
30 |
31 |
32 |
33 | )}
34 |
35 | );
36 | }
37 |
38 | export default EditTournamentWrapper;
39 |
--------------------------------------------------------------------------------
/web-client/client/src/components/form/UsersTypeahead.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import Typeahead from './Typeahead';
3 | import { getAll } from '../../services/users';
4 |
5 | function UsersTypeahead({ excluded, ...rest }) {
6 | const [users, setUsers] = useState([]);
7 | const [loading, setLoading] = useState(true);
8 | const [error, setError] = useState(null);
9 |
10 | useEffect(() => fetchUsers(), []);
11 |
12 | const fetchUsers = (username) => {
13 | setLoading(true);
14 |
15 | return getAll({ page: 0, pageSize: 5, username, excluded })
16 | .then((res) => {
17 | setUsers(res.data.users);
18 | if (error) setError(null);
19 | })
20 | .catch((err) => setError(err.response.data))
21 | .finally(() => setLoading(false));
22 | };
23 |
24 | return (
25 | ({
29 | id: u.id,
30 | value: u.username,
31 | label: u.username,
32 | }))}
33 | onAsyncChange={fetchUsers}
34 | loading={loading}
35 | error={error}
36 | messages={{
37 | loading: 'Récupération des utilisateurs ...',
38 | error: 'Impossible de récupérer les utilisateurs',
39 | noResult: 'Aucun utilisateur trouvé',
40 | }}
41 | />
42 | );
43 | }
44 |
45 | export default UsersTypeahead;
46 |
--------------------------------------------------------------------------------
/web-client/client/src/components/podiums/PodiumListItem.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { faTrophy } from '@fortawesome/free-solid-svg-icons';
4 | import { getPositionColor } from '../../utils/utils';
5 | import { Col } from 'react-grid-system';
6 | import EditPlayerForm from './EditPlayerForm';
7 |
8 | function PodiumListItem({ podium, canManage }) {
9 | const [showForm, setShowForm] = useState(false);
10 |
11 | const openForm = () => {
12 | if (!showForm && canManage) setShowForm(true);
13 | };
14 |
15 | const closeForm = () => {
16 | setShowForm(false);
17 | };
18 |
19 | return (
20 |
21 |
26 | {showForm ? (
27 |
28 | ) : (
29 | <>
30 |
35 | {podium.player}
36 | >
37 | )}
38 |
39 |
40 | );
41 | }
42 |
43 | export default PodiumListItem;
44 |
--------------------------------------------------------------------------------
/web-client/client/src/components/statistics/ParticipantsStatistics.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 | import { useSelector } from 'react-redux';
3 | import _ from 'lodash';
4 | import ChartSkeleton from './ChartSkeleton';
5 | import Chart from '../utils/Chart';
6 | import { getReversedTournaments } from '../../redux/selectors/tournaments';
7 |
8 | function ParticipantsStatistics() {
9 | const tournaments = useSelector(getReversedTournaments);
10 | const { maxItems } = useSelector((state) => state.statistics);
11 | const { loading, error } = useSelector((state) => state.tournaments);
12 |
13 | return loading ? (
14 |
15 | ) : error ? (
16 |
17 |
18 | {error}
19 |
20 |
21 | ) : (
22 |
23 | );
24 | }
25 |
26 | function ParticipantsChart({ tournaments }) {
27 | const series = [
28 | {
29 | data: tournaments.map((t) => t.nbParticipants),
30 | },
31 | ];
32 |
33 | return (
34 | <>
35 |
36 | Évolution du nombre de fleurs
37 |
38 | t.name)}
42 | />
43 | >
44 | );
45 | }
46 |
47 | export default ParticipantsStatistics;
48 |
--------------------------------------------------------------------------------
/web-client/client/src/components/races/RacesSkeleton.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 | import Skeleton from 'react-loading-skeleton';
3 |
4 | function RacesSkeleton() {
5 | return (
6 |
7 |
8 | {[...Array(2)].map((i, index) => (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {[...Array(4)].map((i, index) => (
30 |
34 | ))}
35 |
36 | ))}
37 |
38 |
39 | );
40 | }
41 |
42 | export default RacesSkeleton;
43 |
--------------------------------------------------------------------------------
/api/controllers/podium_ctrl.js:
--------------------------------------------------------------------------------
1 | const db = require('../models');
2 |
3 | module.exports = {
4 | getPodium: (socket, onError, tournamentId) => {
5 | db.Podium.findAll({
6 | where: { TournamentId: tournamentId },
7 | order: [
8 | ['position', 'ASC'],
9 | ['player', 'ASC'],
10 | ],
11 | })
12 | .then((podiums) => socket.emit('getPodium', podiums))
13 | .catch(() => onError('Une erreur est survenue'));
14 | },
15 |
16 | create: (io, socket, onError, { position, player, tournamentId }) => {
17 | db.Podium.create({
18 | position,
19 | player,
20 | TournamentId: tournamentId,
21 | })
22 | .then((podium) => {
23 | socket.emit('closeAddPlayerForm');
24 | io.emit('addPodium', podium);
25 | })
26 | .catch(() => onError('Une erreur est survenue'));
27 | },
28 |
29 | update: (io, socket, onError, { position, player, id }) => {
30 | db.Podium.findByPk(id)
31 | .then((podium) => {
32 | if (podium) {
33 | podium
34 | .update({ position, player })
35 | .then((newPodium) => {
36 | socket.emit('closeEditPlayerForm');
37 | io.emit('editPodium', newPodium);
38 | })
39 | .catch(() => onError('Une erreur est survenue'));
40 | } else {
41 | onError('Une erreur est survenue');
42 | }
43 | })
44 | .catch(() => onError('Une erreur est survenue'));
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/participations/ParticipationSkeleton.js:
--------------------------------------------------------------------------------
1 | import { Row, Col } from 'react-grid-system';
2 | import Skeleton from 'react-loading-skeleton';
3 | import PodiumSkeleton from '../../podiums/PodiumSkeleton';
4 | import ParticipationChartSkeleton from '../../participations/ParticipationChartSkeleton';
5 |
6 | function ParticipationSkeleton({ showPodium = true }) {
7 | return (
8 | <>
9 | {showPodium && }
10 |
11 |
15 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {[...Array(8)].map((i, index) => (
38 |
42 | ))}
43 |
44 | >
45 | );
46 | }
47 |
48 | export default ParticipationSkeleton;
49 |
--------------------------------------------------------------------------------
/web-client/client/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 | https://ponce.tournois-mario-kart.fr/
11 | 2020-07-17T16:29:23+00:00
12 |
13 |
14 | https://ponce.tournois-mario-kart.fr/history
15 | 2020-07-17T16:29:23+00:00
16 |
17 |
18 | https://ponce.tournois-mario-kart.fr/races
19 | 2020-07-17T16:29:23+00:00
20 |
21 |
22 | https://ponce.tournois-mario-kart.fr/statistics
23 | 2020-07-17T16:29:23+00:00
24 |
25 |
26 | https://ponce.tournois-mario-kart.fr/profile
27 | 2020-07-17T16:29:23+00:00
28 |
29 |
30 | https://ponce.tournois-mario-kart.fr/my-history
31 | 2020-07-17T16:29:23+00:00
32 |
33 |
34 | https://ponce.tournois-mario-kart.fr/my-races
35 | 2020-07-17T16:29:23+00:00
36 |
37 |
38 | https://ponce.tournois-mario-kart.fr/my-statistics
39 | 2020-07-17T16:29:23+00:00
40 |
41 |
42 | https://ponce.tournois-mario-kart.fr/cgu
43 | 2021-02-17T22:36:23+00:00
44 |
45 |
--------------------------------------------------------------------------------
/web-client/client/src/components/statistics/Pagination.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { Row, Col, useScreenClass } from 'react-grid-system';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { setMaxItems } from '../../redux/actions/statistics';
5 | import PaginationSkeleton from './PaginationSkeleton';
6 | import { getMaxItemsFromScreenClass } from '../../utils/utils';
7 | import Select from '../form/Select';
8 |
9 | function Pagination() {
10 | const { maxItems, itemsPerPage } = useSelector((state) => state.statistics);
11 | const { loading } = useSelector((state) => state.tournaments);
12 | const dispatch = useDispatch();
13 | const screenClass = useScreenClass();
14 |
15 | useEffect(() => {
16 | dispatch(setMaxItems(getMaxItemsFromScreenClass(screenClass)));
17 | }, [screenClass]);
18 |
19 | const handleMaxItemChange = ({ value }) => {
20 | dispatch(setMaxItems(value));
21 | };
22 |
23 | return loading ? (
24 |
25 | ) : (
26 |
27 |
28 | Afficher les
29 |
42 | );
43 | }
44 |
45 | export default Pagination;
46 |
--------------------------------------------------------------------------------
/web-client/client/src/components/podiums/AddPlayerBtn.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Row, Col } from 'react-grid-system';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 | import { faPlus } from '@fortawesome/free-solid-svg-icons';
5 | import AddPlayerForm from './AddPlayerForm';
6 |
7 | function AddPlayerBtn({ tournamentId }) {
8 | const [showForm, setShowForm] = useState(false);
9 |
10 | const openForm = () => {
11 | if (!showForm) setShowForm(true);
12 | };
13 |
14 | const closeForm = () => {
15 | setShowForm(false);
16 | };
17 |
18 | return (
19 |
20 |
21 |
26 |
27 | {showForm ? (
28 |
32 | ) : (
33 |
34 |
38 | Ajouter au podium
39 |
40 | )}
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
48 | export default AddPlayerBtn;
49 |
--------------------------------------------------------------------------------
/web-client/client/src/components/participations/ParticipationComparisonsChart.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import useComparisons from '../../hooks/useComparisons';
3 | import ParticipationChart from './ParticipationChart';
4 | import ParticipationChartSkeleton from './ParticipationChartSkeleton';
5 | import ParticipationComparison from './ParticipationComparison';
6 |
7 | function ParticipationComparisonsChart({
8 | participation,
9 | nbMaxRaces,
10 | tournamentName,
11 | user,
12 | }) {
13 | const {
14 | comparisons,
15 | onAddComparison,
16 | onRemoveComparison,
17 | loading,
18 | } = useComparisons({
19 | tournament: participation?.TournamentId,
20 | excludedParticipations: participation ? [participation] : undefined,
21 | });
22 |
23 | return loading ? (
24 |
25 | ) : (
26 | <>
27 |
38 |
39 | c.User.id),
44 | participation.UserId,
45 | ]}
46 | />
47 | >
48 | );
49 | }
50 |
51 | export default ParticipationComparisonsChart;
52 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/participations/AddRaceForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import _ from 'lodash';
3 | import { useSelector } from 'react-redux';
4 | import { getNbPointsFromPosition } from '../../../utils/utils';
5 | import RaceForm from './RaceForm';
6 |
7 | function AddRaceForm({ closeForm, participationId }) {
8 | const { tracks } = useSelector((state) => state.tracks);
9 | const { socket } = useSelector((state) => state.socket);
10 | const [loading, setLoading] = useState(false);
11 | const [error, setError] = useState(null);
12 |
13 | useEffect(() => {
14 | socket.on('closeAddRaceForm', () => closeForm());
15 |
16 | return () => socket.off('closeAddRaceForm');
17 | }, []);
18 |
19 | const onSubmit = ({ position, trackName, disconnected }) => {
20 | const track = _.find(tracks, ['name', trackName]);
21 |
22 | if (track) {
23 | setLoading(true);
24 |
25 | socket.emit(
26 | 'addRace',
27 | {
28 | position: parseInt(position),
29 | nbPoints: getNbPointsFromPosition(position),
30 | disconnected,
31 | trackId: track.id,
32 | participationId,
33 | },
34 | (err) => {
35 | setError(err);
36 | setLoading(false);
37 | }
38 | );
39 | } else {
40 | setError("Ce circuit n'existe pas");
41 | }
42 | };
43 |
44 | return (
45 |
51 | );
52 | }
53 |
54 | export default AddRaceForm;
55 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/participations/AddRaceBtn.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Row, Col } from 'react-grid-system';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 | import { faPlus } from '@fortawesome/free-solid-svg-icons';
5 | import AddRaceForm from './AddRaceForm';
6 |
7 | function AddRaceBtn({ participationId }) {
8 | const [showForm, setShowForm] = useState(false);
9 |
10 | const openForm = () => {
11 | if (!showForm) setShowForm(true);
12 | };
13 |
14 | const closeForm = () => {
15 | setShowForm(false);
16 | };
17 |
18 | return (
19 |
20 |
21 |
26 |
27 | {showForm ? (
28 |
32 | ) : (
33 |
34 |
38 | Ajouter une course
39 |
40 | )}
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
48 | export default AddRaceBtn;
49 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/actions/auth.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie';
2 | import { getProfil, signup as _signup } from '../../services/auth';
3 | import {
4 | SET_USER,
5 | SET_LOADING,
6 | ADD_EDITOR,
7 | REMOVE_EDITOR,
8 | } from '../types/auth';
9 |
10 | export const fetchUser = () => (dispatch) => {
11 | getProfil()
12 | .then((res) => dispatch({ type: SET_USER, payload: res.data }))
13 | .catch(() => {})
14 | .finally(() => dispatch({ type: SET_LOADING, payload: false }));
15 | };
16 |
17 | export const updateUser = (user) => (dispatch) => {
18 | dispatch({ type: SET_USER, payload: user });
19 | };
20 |
21 | export const addEditor = (editor) => (dispatch) => {
22 | dispatch({ type: ADD_EDITOR, payload: editor });
23 | };
24 |
25 | export const removeEditor = (editor) => (dispatch) => {
26 | dispatch({ type: REMOVE_EDITOR, payload: editor });
27 | };
28 |
29 | export const signup = (user, setError, setLoading, onSignup) => (dispatch) => {
30 | setLoading(true);
31 | _signup(user)
32 | .then((res) => {
33 | dispatch({ type: SET_USER, payload: res.data.user });
34 | Cookies.set('token', res.data.token, { expires: 365 });
35 | onSignup();
36 | })
37 | .catch((err) => {
38 | setError(err.response.data);
39 | setLoading(false);
40 | });
41 | };
42 |
43 | export const signin = (token, onSignin) => (dispatch) => {
44 | Cookies.set('token', token, { expires: 365 });
45 | getProfil()
46 | .then((res) => dispatch({ type: SET_USER, payload: res.data }))
47 | .finally(() => onSignin());
48 | };
49 |
50 | export const signout = (onSignout) => (dispatch) => {
51 | Cookies.remove('token');
52 | dispatch({ type: SET_USER, payload: null });
53 | onSignout();
54 | };
55 |
--------------------------------------------------------------------------------
/web-client/client/src/assets/sass/tournaments/_index.scss:
--------------------------------------------------------------------------------
1 | .home__buttons {
2 | margin-top: 3rem;
3 | }
4 |
5 | .tournament__title,
6 | .tournamentsList__title {
7 | margin-top: 2rem;
8 | }
9 |
10 | .tournament__podium,
11 | .tournamentsList__tournament {
12 | margin: 1rem 0;
13 | padding: 1rem;
14 | border-radius: 6px;
15 | text-align: center;
16 | border: 1px solid var(--border-color);
17 | transition: box-shadow 0.3s ease;
18 | cursor: pointer;
19 | }
20 |
21 | .tournament__podium:hover,
22 | .tournamentsList__tournament:hover {
23 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.15);
24 | }
25 |
26 | .tournamentsList__tournament--skeleton {
27 | margin: 1rem 0;
28 | padding: 2rem 0rem;
29 | }
30 |
31 | .tournamentsList__tournamentName {
32 | margin: 0;
33 | margin-bottom: 0.25rem;
34 | }
35 |
36 | .tournamentsList__tournamentDates {
37 | cursor: pointer;
38 | color: var(--tertiary-text-color);
39 | font-style: italic;
40 | }
41 |
42 | .tournament__infos {
43 | border: 1px solid var(--border-color);
44 | border-radius: 6px;
45 | padding: 0 1rem;
46 | margin: 0.5rem 0;
47 | }
48 |
49 | .tournament__infos--skeleton {
50 | margin: 0.5rem 0;
51 | }
52 |
53 | .tournament__info {
54 | margin: 1rem 0;
55 | }
56 |
57 | .tournament__info > label {
58 | display: block;
59 | color: var(--tertiary-text-color);
60 | }
61 |
62 | .tournament__info > h4,
63 | .tournament__setNbPoints {
64 | margin: 0.5rem 0 0 0;
65 | }
66 |
67 | .tournament__setNbPoints {
68 | display: inline-block;
69 | }
70 |
71 | .tournament__setNbPoints--canAdd {
72 | cursor: pointer;
73 | }
74 |
75 | .tournament__nbPoints {
76 | display: inline-block;
77 | margin: 0;
78 | }
79 |
80 | .tournament__setNbPointsIcon {
81 | color: var(--main-color);
82 | margin-right: 1rem;
83 | }
84 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/participations/EditRaceForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import _ from 'lodash';
3 | import { useSelector } from 'react-redux';
4 | import { getNbPointsFromPosition } from '../../../utils/utils';
5 | import RaceForm from './RaceForm';
6 |
7 | function EditRaceForm({ closeForm, race }) {
8 | const { tracks } = useSelector((state) => state.tracks);
9 | const { socket } = useSelector((state) => state.socket);
10 | const [loading, setLoading] = useState(false);
11 | const [error, setError] = useState(null);
12 |
13 | useEffect(() => {
14 | socket.on('closeEditRaceForm', () => closeForm());
15 |
16 | return () => socket.off('closeEditRaceForm');
17 | }, []);
18 |
19 | const onSubmit = ({ position, trackName, disconnected }) => {
20 | const track = _.find(tracks, ['name', trackName]);
21 |
22 | if (track) {
23 | setLoading(true);
24 |
25 | socket.emit(
26 | 'editRace',
27 | {
28 | position: parseInt(position),
29 | nbPoints: getNbPointsFromPosition(position),
30 | disconnected,
31 | trackId: track.id,
32 | raceId: race.id,
33 | },
34 | (err) => {
35 | setError(err);
36 | setLoading(false);
37 | }
38 | );
39 | } else {
40 | setError("Ce circuit n'existe pas");
41 | }
42 | };
43 |
44 | return (
45 |
52 | );
53 | }
54 |
55 | export default EditRaceForm;
56 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/patchNotes/AddPatchNoteForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { useHistory } from 'react-router-dom';
4 | import { Col, Row } from 'react-grid-system';
5 | import { Helmet } from 'react-helmet';
6 | import PatchNoteForm from './PatchNoteForm';
7 | import { create } from '../../../services/patchNotes';
8 | import { addPatchNote } from '../../../redux/actions/patchNotes';
9 |
10 | function AddPatchNoteForm() {
11 | const [loading, setLoading] = useState(false);
12 | const [error, setError] = useState(null);
13 | const dispatch = useDispatch();
14 | const history = useHistory();
15 |
16 | const onSubmit = (patchNote) => {
17 | setLoading(true);
18 | setError(null);
19 |
20 | create(patchNote)
21 | .then((res) => {
22 | dispatch(addPatchNote(res.data));
23 | history.push('/admin/patch-notes');
24 | })
25 | .catch((err) => {
26 | setError(err.response.data);
27 | setLoading(false);
28 | });
29 | };
30 |
31 | return (
32 |
33 |
34 | Créer un patch note
35 |
36 |
37 |
38 |
39 | Créer un patch note
40 |
41 | history.push('/admin/patch-notes')}
44 | loading={loading}
45 | error={error}
46 | />
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | export default AddPatchNoteForm;
54 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tracks/TracksWrapper.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Row, Col } from 'react-grid-system';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import TracksListItem from './TracksListItem';
5 | import AddTrackBtn from './AddTrackBtn';
6 | import AddTrackForm from './AddTrackForm';
7 | import { addTrack as addNewTrack } from '../../../redux/actions/tracks';
8 |
9 | function TracksWrapper({ cup }) {
10 | const dispatch = useDispatch();
11 | const { tracks: allTracks } = useSelector((state) => state.tracks);
12 | const [tracks, setTracks] = useState([]);
13 | const [creating, setCreating] = useState(false);
14 |
15 | useEffect(() => {
16 | setCreating(false);
17 | setTracks(allTracks.filter((track) => track.CupId === cup.id));
18 | }, [cup]);
19 |
20 | const addTrack = (track) => {
21 | dispatch(addNewTrack(track));
22 | setTracks([...tracks, track]);
23 | };
24 |
25 | return creating ? (
26 |
27 |
28 | Ajouter une circuit
29 |
34 |
35 |
36 | ) : (
37 | <>
38 | Circuits de la coupe {cup.name}
39 |
40 |
41 | {tracks.map((track) => (
42 |
43 | ))}
44 |
45 | {tracks.length < 4 &&
46 | [...Array(4 - tracks.length)].map((i, index) => (
47 |
48 | ))}
49 |
50 | >
51 | );
52 | }
53 |
54 | export default TracksWrapper;
55 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tournaments/EditTournamentForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { Helmet } from 'react-helmet';
3 | import { useHistory } from 'react-router-dom';
4 | import { useSelector } from 'react-redux';
5 | import TournamentForm from './TournamentForm';
6 | import { nullifyEmptyFields, serializeTournament } from '../../../utils/utils';
7 |
8 | function EditTournamentForm({ tournament }) {
9 | const { socket } = useSelector((state) => state.socket);
10 | const [loading, setLoading] = useState(false);
11 | const [error, setError] = useState(null);
12 | const history = useHistory();
13 |
14 | const onSubmit = (newTournament) => {
15 | setLoading(true);
16 |
17 | socket.emit(
18 | 'updateTournament',
19 | {
20 | ...nullifyEmptyFields(serializeTournament(newTournament)),
21 | id: tournament.id,
22 | },
23 | (err) => {
24 | setError(err);
25 | setLoading(false);
26 | }
27 | );
28 | };
29 |
30 | useEffect(() => {
31 | socket.on('updateTournament', (tournament) => {
32 | setLoading(false);
33 | history.push(`/admin/tournaments/${tournament.id}`);
34 | });
35 | }, []);
36 |
37 | return (
38 | <>
39 |
40 | Modifier un tournoi
41 |
42 |
43 | Modifier un tournoi
44 |
45 |
49 | history.push(`/admin/tournaments/${tournament.id}`)
50 | }
51 | loading={loading}
52 | error={error}
53 | />
54 | >
55 | );
56 | }
57 |
58 | export default EditTournamentForm;
59 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tournaments/AddTournamentForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { Helmet } from 'react-helmet';
3 | import { useSelector } from 'react-redux';
4 | import { useHistory } from 'react-router-dom';
5 | import { Row, Col } from 'react-grid-system';
6 | import TournamentForm from './TournamentForm';
7 | import { nullifyEmptyFields, serializeTournament } from '../../../utils/utils';
8 |
9 | function AddTournamentForm() {
10 | const { socket } = useSelector((state) => state.socket);
11 | const [loading, setLoading] = useState(false);
12 | const [error, setError] = useState(null);
13 | const history = useHistory();
14 |
15 | const onSubmit = (tournament) => {
16 | setLoading(true);
17 |
18 | socket.emit(
19 | 'createTournament',
20 | nullifyEmptyFields(serializeTournament(tournament)),
21 | (err) => {
22 | setError(err);
23 | setLoading(false);
24 | }
25 | );
26 | };
27 |
28 | useEffect(() => {
29 | socket.on('createTournament', (tournament) => {
30 | setLoading(false);
31 | history.push(`/admin/tournaments/${tournament.id}`);
32 | });
33 | }, []);
34 |
35 | return (
36 |
37 |
38 | Créer un tournoi
39 |
40 |
41 |
42 |
43 | Créer un tournoi
44 |
45 | history.push('/admin/tournaments')}
48 | loading={loading}
49 | error={error}
50 | />
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | export default AddTournamentForm;
58 |
--------------------------------------------------------------------------------
/web-client/client/src/utils/style.js:
--------------------------------------------------------------------------------
1 | export const CSSTheme = {
2 | light: {
3 | mainColor: '#ff56a9',
4 | mainColorLight: '#fda3cf',
5 | mainBackgroundColor: '#ffffff',
6 | secondaryBackgroundColor: '#f3f3f4',
7 | mainTextColor: '#1d1d1d',
8 | tertiaryTextColor: '#989aa0',
9 | borderColor: '#e6e6e6',
10 | successColor: '#68b684',
11 | errorColor: '#f3453f',
12 | worstChartColor: '#ea4335',
13 | },
14 | dark: {
15 | mainColor: '#ff56a9',
16 | mainColorLight: '#fda3cf',
17 | mainBackgroundColor: '#36393f',
18 | secondaryBackgroundColor: '#2f3136',
19 | mainTextColor: '#ffffff',
20 | tertiaryTextColor: '#989aa0',
21 | borderColor: '#202225',
22 | successColor: '#68b684',
23 | errorColor: '#f3453f',
24 | worstChartColor: '#ea4335',
25 | },
26 | };
27 |
28 | export const getSelectStyle = (defaultStyle, theme) => ({
29 | ...defaultStyle,
30 | borderRadius: 6,
31 | colors: {
32 | ...defaultStyle.colors,
33 | primary: CSSTheme[theme].mainColor,
34 | primary25: CSSTheme[theme].secondaryBackgroundColor,
35 | primary50: CSSTheme[theme].mainColorLight,
36 | neutral0: CSSTheme[theme].mainBackgroundColor,
37 | neutral20: CSSTheme[theme].borderColor,
38 | neutral30: CSSTheme[theme].borderColor,
39 | neutral40: CSSTheme[theme].borderColor,
40 | neutral60: CSSTheme[theme].borderColor,
41 | neutral80: CSSTheme[theme].mainTextColor,
42 | neutral90: CSSTheme[theme].mainTextColor,
43 | },
44 | });
45 |
46 | export const COMPARISONS_COLORS = [
47 | '#33658a',
48 | '#04724D',
49 | '#F26419',
50 | '#7A8450',
51 | '#AA78A6',
52 | '#5F5449',
53 | '#B09398',
54 | '#440D0F',
55 | '#93E1D8',
56 | '#F6AE2D',
57 | '#4CB963',
58 | '#C73E1D',
59 | '#C59849',
60 | '#9B5DE5',
61 | '#CBE896',
62 | '#00BBF9',
63 | '#9A8873',
64 | '#610345',
65 | '#E3B505',
66 | '#044B7F',
67 | ];
68 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/patchNotes/PatchNotes.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { Col, Row } from 'react-grid-system';
3 | import { Helmet } from 'react-helmet';
4 | import { Link } from 'react-router-dom';
5 | import PatchNoteListItem from './PatchNoteListItem';
6 | import PatchNotesSkeleton from './PatchNotesSkeleton';
7 |
8 | function PatchNotes() {
9 | const { patchNotes, loading, error } = useSelector(
10 | (state) => state.patchNotes
11 | );
12 |
13 | return (
14 |
15 |
16 | Patch notes
17 |
18 |
19 | {loading ? (
20 |
21 | ) : error ? (
22 |
23 |
24 |
25 | {error}
26 |
27 |
28 |
29 | ) : (
30 | <>
31 |
32 |
33 |
37 | Créer un patch note
38 |
39 |
40 |
41 |
42 |
Patch notes
43 |
44 |
45 | {patchNotes.map((patchNote) => (
46 |
50 | ))}
51 |
52 | >
53 | )}
54 |
55 | );
56 | }
57 | export default PatchNotes;
58 |
--------------------------------------------------------------------------------
/web-client/client/src/components/races/RacesListItem.js:
--------------------------------------------------------------------------------
1 | import { Col, Row } from 'react-grid-system';
2 |
3 | function RacesListItem({ cup }) {
4 | return (
5 | <>
6 | Coupe {cup.name}
7 |
8 |
9 |
10 |
11 |
12 |
13 | Circuit
14 |
15 | Nombre de fois joué
16 | Moyenne de points
17 |
18 | Position moyenne
19 |
20 |
21 |
22 |
23 |
24 |
25 | {cup.Tracks.map((track) => (
26 |
27 | ))}
28 | >
29 | );
30 | }
31 |
32 | function TracksListItem({ track }) {
33 | const { nbPoints, nbPlayed, position } = track.statistics;
34 | const averagePoints = nbPlayed ? (nbPoints / nbPlayed).toFixed(1) : 0;
35 | const averagePosition = nbPlayed ? (position / nbPlayed).toFixed(1) : 0;
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
43 | {track.name}
44 |
45 | {nbPlayed}
46 | {averagePoints}
47 |
48 | {averagePosition}
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | export default RacesListItem;
58 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tournaments/TournamentsWrapper.js:
--------------------------------------------------------------------------------
1 | import { Helmet } from 'react-helmet';
2 | import { Row, Col } from 'react-grid-system';
3 | import { Link } from 'react-router-dom';
4 | import { useSelector } from 'react-redux';
5 | import TournamentsListItem from './TournamentsListItem';
6 | import TournamentsSkeleton from './TournamentsSkeleton';
7 |
8 | function TournamentsWrapper() {
9 | const { tournaments, loading, error } = useSelector(
10 | (state) => state.tournaments
11 | );
12 |
13 | return (
14 |
15 |
16 | Tournois
17 |
18 |
19 | {loading ? (
20 |
21 | ) : error ? (
22 |
23 |
24 |
25 | {error}
26 |
27 |
28 |
29 | ) : (
30 | <>
31 |
32 |
33 |
37 | Créer un tournoi
38 |
39 |
40 |
41 |
42 |
Tournois
43 |
44 |
45 | {tournaments.map((tournament) => (
46 |
50 | ))}
51 |
52 | >
53 | )}
54 |
55 | );
56 | }
57 |
58 | export default TournamentsWrapper;
59 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/patchNotes/EditPatchNoteForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Col, Row } from 'react-grid-system';
3 | import { Helmet } from 'react-helmet';
4 | import { useDispatch } from 'react-redux';
5 | import { useHistory } from 'react-router-dom';
6 | import PatchNoteForm from './PatchNoteForm';
7 | import { updateById } from '../../../services/patchNotes';
8 | import { editPatchNote } from '../../../redux/actions/patchNotes';
9 |
10 | function EditPatchNoteForm({ patchNote }) {
11 | const [loading, setLoading] = useState(false);
12 | const [error, setError] = useState(null);
13 | const dispatch = useDispatch();
14 | const history = useHistory();
15 |
16 | const onSubmit = (newPatchNote) => {
17 | setLoading(true);
18 | setError(null);
19 |
20 | updateById(patchNote.id, newPatchNote)
21 | .then((res) => {
22 | dispatch(editPatchNote(res.data));
23 | history.push(`/admin/patch-notes/${patchNote.id}`);
24 | })
25 | .catch((err) => {
26 | setError(err.response.data);
27 | setLoading(false);
28 | });
29 | };
30 |
31 | return (
32 |
33 |
34 | Modifier un patch note
35 |
36 |
37 |
38 |
39 |
40 | Modifier un patch note
41 |
42 |
43 |
46 | history.push(`/admin/patch-notes/${patchNote.id}`)
47 | }
48 | patchNote={patchNote}
49 | loading={loading}
50 | error={error}
51 | />
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | export default EditPatchNoteForm;
59 |
--------------------------------------------------------------------------------
/api/index.js:
--------------------------------------------------------------------------------
1 | const { formatWebClientURLForCORS } = require('./utils');
2 |
3 | require('dotenv').config();
4 | const express = require('express'),
5 | app = express(),
6 | server = require('http').createServer(app),
7 | io = require('socket.io')(server, {
8 | transports: ['websocket'],
9 | }),
10 | redisAdapter = require('socket.io-redis'),
11 | cors = require('cors'),
12 | helmet = require('helmet'),
13 | cluster = require('cluster');
14 |
15 | const { NODE_ENV, PORT, REDIS_URL, WEB_CONCURRENCY } = process.env;
16 |
17 | if (cluster.isMaster && NODE_ENV !== 'test') {
18 | const numWorkers = WEB_CONCURRENCY || 1;
19 | console.log(`Master cluster setting up ${numWorkers} workers....`);
20 |
21 | for (let i = 0; i < numWorkers; i += 1) {
22 | cluster.fork();
23 | }
24 |
25 | cluster.on(`online`, (worker) => {
26 | console.log(`Worker ${worker.process.pid} is online`);
27 | });
28 |
29 | cluster.on(`exit`, (worker) => {
30 | console.log(`Worker ${worker.process.pid} died `);
31 | cluster.fork();
32 | });
33 | } else {
34 | io.adapter(redisAdapter(REDIS_URL));
35 |
36 | app.use(express.json());
37 | app.use(express.urlencoded({ extended: true }));
38 | app.use(
39 | cors({
40 | origin: formatWebClientURLForCORS(),
41 | })
42 | );
43 | app.use(helmet());
44 |
45 | const errorHandler = (err, req, res, next) => {
46 | if (err.status === null || err.status === undefined) {
47 | res.status(500).send(err.message);
48 | } else {
49 | res.status(err.status).send(err.message);
50 | }
51 | };
52 |
53 | require('./passport');
54 | require('./routes-api')(app);
55 |
56 | io.on('connection', (socket) => {
57 | const { userId, isAdmin } = socket.handshake.query;
58 | require('./routes-socket')(
59 | io,
60 | socket,
61 | userId ? parseInt(userId) : null,
62 | isAdmin === 'true'
63 | );
64 | });
65 |
66 | app.use(errorHandler);
67 |
68 | server.listen(PORT, () => {
69 | console.log('Application lancée sur le port ' + PORT);
70 | });
71 | }
72 |
--------------------------------------------------------------------------------
/api/models/tournament.js:
--------------------------------------------------------------------------------
1 | const { Model } = require('sequelize');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | class Tournament extends Model {
5 | static nameIsUnique(name, id = null) {
6 | return this.findOne({ where: { name } })
7 | .then((c) => (c ? c.id === id : true))
8 | .catch((err) => false);
9 | }
10 | }
11 |
12 | Tournament.init(
13 | {
14 | name: {
15 | type: DataTypes.STRING,
16 | allowNull: false,
17 | unique: true,
18 | validate: { len: [3, 50] },
19 | },
20 | nbParticipants: {
21 | type: DataTypes.INTEGER,
22 | validate: { min: 1 },
23 | },
24 | nbMaxRaces: {
25 | type: DataTypes.INTEGER,
26 | allowNull: false,
27 | validate: { min: 1 },
28 | },
29 | startDate: {
30 | type: DataTypes.DATE,
31 | allowNull: false,
32 | },
33 | endDate: {
34 | type: DataTypes.DATE,
35 | allowNull: false,
36 | },
37 | },
38 | {
39 | sequelize,
40 | modelName: 'Tournament',
41 | timestamps: false,
42 | }
43 | );
44 |
45 | Tournament.associate = (db) => {
46 | Tournament.hasMany(db.Participation);
47 | Tournament.hasMany(db.Podium);
48 | Tournament.belongsToMany(db.User, {
49 | through: db.StreamersChart,
50 | as: 'Streamers',
51 | foreignKey: 'TournamentId',
52 | });
53 | };
54 |
55 | Tournament.afterCreate((tournament) => {
56 | sequelize.models.User.findAll({ attributes: ['id'] })
57 | .then((users) => {
58 | const participations = users.map((user) => ({
59 | TournamentId: tournament.id,
60 | UserId: user.id,
61 | }));
62 |
63 | sequelize.models.Participation.bulkCreate(participations);
64 | })
65 | .catch(() => {});
66 | });
67 |
68 | return Tournament;
69 | };
70 |
--------------------------------------------------------------------------------
/web-client/client/src/components/utils/Tabs.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useState } from 'react';
2 | import { Col, Row } from 'react-grid-system';
3 | import { createContext } from '../../utils/createContext';
4 |
5 | const [TabsProvider, useTabsContext] = createContext();
6 |
7 | const useTabs = ({ defaultTab, onTabChange, align }) => {
8 | const [selectedTab, setSelectedTab] = useState(defaultTab);
9 |
10 | const handleTabChange = (tab) => {
11 | if (onTabChange) onTabChange(tab);
12 | setSelectedTab(tab);
13 | };
14 |
15 | return {
16 | align,
17 | selectedTab,
18 | setSelectedTab: handleTabChange,
19 | };
20 | };
21 |
22 | function Tabs({ defaultTab, align, onTabChange, children }) {
23 | const ctx = useTabs({ defaultTab, align, onTabChange });
24 | const context = useMemo(() => ctx, [ctx]);
25 |
26 | return {children};
27 | }
28 |
29 | function TabsList({ tabs }) {
30 | const { align, selectedTab, setSelectedTab } = useTabsContext();
31 |
32 | const handleTabChange = (tab) => {
33 | if (tab.value !== selectedTab) setSelectedTab(tab.value);
34 | };
35 |
36 | return (
37 |
38 | {tabs.map((tab) => (
39 |
40 |
53 |
54 | ))}
55 |
56 | );
57 | }
58 |
59 | function Tab({ value, children = null }) {
60 | const { selectedTab } = useTabsContext();
61 |
62 | return selectedTab === value ? children : null;
63 | }
64 |
65 | Tabs.Tab = Tab;
66 | Tabs.TabsList = TabsList;
67 |
68 | export default Tabs;
69 |
--------------------------------------------------------------------------------
/api/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and not Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | # Stores VSCode versions used for testing VSCode extensions
107 | .vscode-test
108 |
--------------------------------------------------------------------------------
/web-client/client/src/components/participations/ParticipationChartLegends.js:
--------------------------------------------------------------------------------
1 | import { faTrashAlt } from '@fortawesome/free-solid-svg-icons';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import React from 'react';
4 |
5 | function ParticipationChartLegends({
6 | series,
7 | hiddenSeries,
8 | onShow,
9 | onHide,
10 | onRemove,
11 | }) {
12 | return (
13 |
14 | {series.map((serie) => {
15 | const isHidden = hiddenSeries.includes(serie.name);
16 |
17 | return (
18 |
22 |
29 | isHidden
30 | ? onShow(serie.name)
31 | : onHide(serie.name)
32 | }
33 | >
34 |
38 |
39 | {serie.name}
40 | {serie.tournament && ` (${serie.tournament})`}
41 |
42 |
43 | {serie.deletable && onRemove && (
44 |
onRemove(serie.name)}
48 | />
49 | )}
50 |
51 | );
52 | })}
53 |
54 | );
55 | }
56 |
57 | export default ParticipationChartLegends;
58 |
--------------------------------------------------------------------------------
/web-client/client/src/components/patchNotes/LatestPatchNote.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import moment from 'moment';
3 | import { motion } from 'framer-motion';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 | import { faTimes } from '@fortawesome/free-solid-svg-icons';
6 | import { Row, Col } from 'react-grid-system';
7 | import Markdown from 'react-markdown';
8 |
9 | function LatestPatchNote({ patchNote, onClose }) {
10 | useEffect(() => {
11 | document.body.style.overflow = 'hidden';
12 |
13 | return () => (document.body.style.overflow = 'auto');
14 | }, []);
15 |
16 | return (
17 |
24 | e.stopPropagation()}
27 | initial={{ opacity: 0, y: -100 }}
28 | animate={{ opacity: 1, y: 0 }}
29 | exit={{ opacity: 0, y: -100 }}
30 | >
31 |
32 |
33 |
34 | Nouveautés du{' '}
35 | {moment(patchNote.createdAt).format('DD MMMM YYYY')}
36 |
37 |
38 | Version {patchNote.version}
39 |
40 |
41 |
42 |
47 |
48 |
49 |
50 |
51 | {patchNote.content}
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | export default LatestPatchNote;
59 |
--------------------------------------------------------------------------------
/api/models/user.js:
--------------------------------------------------------------------------------
1 | const { Model } = require('sequelize');
2 |
3 | module.exports = (sequelize, DataTypes) => {
4 | class User extends Model {
5 | static usernameIsUnique(username) {
6 | return this.findOne({ where: { username } })
7 | .then((u) => (u ? false : true))
8 | .catch((err) => false);
9 | }
10 | }
11 |
12 | User.init(
13 | {
14 | username: {
15 | type: DataTypes.STRING,
16 | unique: true,
17 | allowNull: false,
18 | validate: { len: [3, 50] },
19 | },
20 | twitchId: {
21 | type: DataTypes.STRING,
22 | unique: true,
23 | allowNull: false,
24 | },
25 | isAdmin: {
26 | type: DataTypes.BOOLEAN,
27 | allowNull: false,
28 | defaultValue: false,
29 | },
30 | },
31 | {
32 | sequelize,
33 | modelName: 'User',
34 | timestamps: true,
35 | updatedAt: false,
36 | }
37 | );
38 |
39 | User.associate = (db) => {
40 | User.hasMany(db.Participation);
41 | User.belongsToMany(db.User, {
42 | through: db.ManagersEditors,
43 | as: 'Editors',
44 | foreignKey: 'ManagerId',
45 | });
46 | User.belongsToMany(db.User, {
47 | through: db.ManagersEditors,
48 | as: 'Managers',
49 | foreignKey: 'EditorId',
50 | });
51 | User.belongsToMany(db.Tournament, {
52 | through: db.StreamersChart,
53 | as: 'TournamentsAsStreamer',
54 | foreignKey: 'StreamerId',
55 | });
56 | };
57 |
58 | User.afterCreate((user) => {
59 | sequelize.models.Tournament.findAll({ attributes: ['id'] })
60 | .then((tournaments) => {
61 | const participations = tournaments.map((tournament) => ({
62 | TournamentId: tournament.id,
63 | UserId: user.id,
64 | }));
65 |
66 | sequelize.models.Participation.bulkCreate(participations);
67 | })
68 | .catch(() => {});
69 | });
70 |
71 | return User;
72 | };
73 |
--------------------------------------------------------------------------------
/api/utils.js:
--------------------------------------------------------------------------------
1 | const db = require('./models');
2 |
3 | module.exports = {
4 | formatWebClientURLForCORS: () => {
5 | const { WEB_CLIENT_URL } = process.env;
6 | if (!WEB_CLIENT_URL) return [];
7 |
8 | try {
9 | const protocols = ['http://', 'https://'];
10 | const { host } = new URL(WEB_CLIENT_URL);
11 | return protocols.map((protocol) => `${protocol}${host}`);
12 | } catch (err) {
13 | return [WEB_CLIENT_URL];
14 | }
15 | },
16 |
17 | paginate: (page, pageSize) => {
18 | if (isNaN(page) || isNaN(pageSize)) return {};
19 |
20 | const offset = page * pageSize;
21 | const limit = pageSize;
22 |
23 | return {
24 | offset,
25 | limit,
26 | };
27 | },
28 |
29 | getPonce: (onError, cb) => {
30 | const { PONCE_TWITCH_ID } = process.env;
31 |
32 | db.User.findOne({ where: { twitchId: PONCE_TWITCH_ID } })
33 | .then((user) => {
34 | if (user) {
35 | cb(user);
36 | } else {
37 | onError('Une erreur est survenue');
38 | }
39 | })
40 | .catch(() => onError('Une erreur est survenue'));
41 | },
42 |
43 | getUser: (onError, userId, cb) => {
44 | db.User.findByPk(userId)
45 | .then((user) => {
46 | if (user) {
47 | cb(user);
48 | } else {
49 | onError("Cet utilisateur n'existe pas");
50 | }
51 | })
52 | .catch(() => onError('Une erreur est survenue'));
53 | },
54 |
55 | canUserManage: (userId, to) => {
56 | return db.User.findByPk(userId, {
57 | include: [
58 | {
59 | model: db.User,
60 | as: 'Managers',
61 | attributes: ['id'],
62 | },
63 | ],
64 | })
65 | .then((user) => {
66 | if (!user) return false;
67 | if (user.isAdmin) return true;
68 | if (user.id === to) return true;
69 | return !!user.Managers.find((m) => m.id === to);
70 | })
71 | .catch(() => false);
72 | },
73 | };
74 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/tournaments/TournamentSkeleton.js:
--------------------------------------------------------------------------------
1 | import { Row, Col, Hidden, useScreenClass } from 'react-grid-system';
2 | import Skeleton from 'react-loading-skeleton';
3 |
4 | function TournamentSkeleton({ showHistory = true, showEdit = false }) {
5 | const screenClass = useScreenClass();
6 |
7 | return (
8 |
9 |
10 | {showHistory && (
11 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
35 |
36 |
37 |
38 | )}
39 |
40 | {showEdit && (
41 |
42 |
43 |
44 |
45 |
46 | )}
47 |
48 |
49 |
50 |
51 |
52 |
56 |
57 |
58 | );
59 | }
60 |
61 | export default TournamentSkeleton;
62 |
--------------------------------------------------------------------------------
/web-client/client/src/redux/reducers/useStreamersChart.js:
--------------------------------------------------------------------------------
1 | import { getComparisonColor, isComparisonUnique } from '../../utils/utils';
2 | import {
3 | SET_LOADING_COMPARISONS,
4 | SET_LOADING_STREAMERS,
5 | SET_STREAMERS,
6 | SET_STREAMERS_COMPARISONS,
7 | ON_GET_PARTICIPATIONS,
8 | RESET_STATE,
9 | } from '../types/useStreamersChart';
10 |
11 | const initialState = {
12 | streamers: [],
13 | loadingStreamers: true,
14 | streamersComparisons: [],
15 | loadingComparisons: true,
16 | };
17 |
18 | export default function (state = initialState, action) {
19 | switch (action.type) {
20 | case RESET_STATE:
21 | return initialState;
22 | case ON_GET_PARTICIPATIONS:
23 | const streamersComparisons = state.streamersComparisons.filter(
24 | (c) => !action.payload.some((p) => p.UserId === c.UserId)
25 | );
26 |
27 | const { comparisons } = action.payload.reduce(
28 | (acc, curr) => {
29 | if (!isComparisonUnique(curr, streamersComparisons))
30 | return acc;
31 |
32 | const color = getComparisonColor(acc.alreadyUsedColors);
33 | return {
34 | comparisons: [...acc.comparisons, { ...curr, color }],
35 | alreadyUsedColors: [...acc.alreadyUsedColors, color],
36 | };
37 | },
38 | {
39 | comparisons: [],
40 | alreadyUsedColors: streamersComparisons.map((c) => c.color),
41 | }
42 | );
43 |
44 | return {
45 | ...state,
46 | loadingComparisons: false,
47 | streamersComparisons: [...streamersComparisons, ...comparisons],
48 | };
49 | case SET_STREAMERS:
50 | return { ...state, streamers: action.payload };
51 | case SET_LOADING_STREAMERS:
52 | return { ...state, loadingStreamers: action.payload };
53 | case SET_STREAMERS_COMPARISONS:
54 | return { ...state, streamersComparisons: action.payload };
55 | case SET_LOADING_COMPARISONS:
56 | return { ...state, loadingComparisons: action.payload };
57 | default:
58 | return state;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/web-client/client/src/components/participations/EditParticipationForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import Form from '../form/Form';
4 | import { Row, Col } from 'react-grid-system';
5 | import Button from '../form/Button';
6 | import { nullifyEmptyFields } from '../../utils/utils';
7 |
8 | function EditParticipationForm({ closeForm, participation, children }) {
9 | const { socket } = useSelector((state) => state.socket);
10 | const [loading, setLoading] = useState(false);
11 | const [error, setError] = useState(null);
12 |
13 | useEffect(() => {
14 | socket.on('closeEditParticipationForm', () => closeForm());
15 |
16 | return () => socket.off('closeEditParticipationForm');
17 | }, []);
18 |
19 | const onSubmit = (newParticipation) => {
20 | setLoading(true);
21 |
22 | socket.emit(
23 | 'editParticipation',
24 | nullifyEmptyFields({
25 | ...newParticipation,
26 | participationId: participation.id,
27 | }),
28 | (err) => {
29 | setError(err);
30 | setLoading(false);
31 | }
32 | );
33 | };
34 |
35 | return (
36 |
66 | );
67 | }
68 |
69 | export default EditParticipationForm;
70 |
--------------------------------------------------------------------------------
/web-client/client/src/components/tournaments/TournamentInfos.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import moment from 'moment';
3 | import { Row, Col } from 'react-grid-system';
4 | import { useSelector } from 'react-redux';
5 |
6 | function TournamentInfos({ defaultTournament }) {
7 | const { socket } = useSelector((state) => state.socket);
8 | const [tournament, setTournament] = useState(defaultTournament);
9 |
10 | useEffect(() => {
11 | socket.on('updateTournament', (newTournament) => {
12 | setTournament((currentTournament) =>
13 | newTournament.id === currentTournament.id
14 | ? newTournament
15 | : currentTournament
16 | );
17 | });
18 | }, []);
19 |
20 | useEffect(() => setTournament(defaultTournament), [defaultTournament]);
21 |
22 | const formatDate = (date) => {
23 | return moment(date).format('DD MMM YYYY à HH:mm');
24 | };
25 |
26 | return (
27 | <>
28 | {tournament.name}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
{formatDate(tournament.startDate)}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
{formatDate(tournament.endDate)}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
{tournament.nbParticipants || '-'}
52 |
53 |
54 |
55 |
56 |
57 |
58 | >
59 | );
60 | }
61 |
62 | export default TournamentInfos;
63 |
--------------------------------------------------------------------------------
/web-client/client/src/components/participations/ParticipationStreamersChart.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import useComparisons from '../../hooks/useComparisons';
4 | import useStreamersChart from '../../hooks/useStreamersChart';
5 | import AddParticipationStreamersChart from './AddParticipationStreamersChart';
6 | import ParticipationChart from './ParticipationChart';
7 | import ParticipationChartSkeleton from './ParticipationChartSkeleton';
8 |
9 | function ParticipationStreamersChart({
10 | participation,
11 | nbMaxRaces,
12 | tournamentName,
13 | user,
14 | }) {
15 | const { user: currentUser } = useSelector((state) => state.auth);
16 | const { socket } = useSelector((state) => state.socket);
17 | const {
18 | loadingStreamers,
19 | loadingComparisons,
20 | streamersComparisons,
21 | } = useStreamersChart({
22 | tournament: participation?.TournamentId,
23 | excludedParticipations: participation ? [participation] : undefined,
24 | });
25 |
26 | const removeFromStreamersChart = (streamer) =>
27 | socket.emit(
28 | 'removeFromStreamersChart',
29 | {
30 | username: streamer,
31 | tournament: participation?.TournamentId,
32 | },
33 | console.error
34 | );
35 |
36 | return loadingStreamers || loadingComparisons ? (
37 |
38 | ) : (
39 | <>
40 |
53 |
54 | {currentUser?.isAdmin && (
55 | c.User.id),
59 | participation.UserId,
60 | ]}
61 | />
62 | )}
63 | >
64 | );
65 | }
66 |
67 | export default ParticipationStreamersChart;
68 |
--------------------------------------------------------------------------------
/web-client/client/src/components/admin/cups/AddCupForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Row, Col } from 'react-grid-system';
3 | import Form from '../../form/Form';
4 | import Input from '../../form/Input';
5 | import Button from '../../form/Button';
6 | import { create } from '../../../services/cups';
7 |
8 | const NAME_LENGTH = 'Le nom doit faire entre 3 et 50 caractères';
9 |
10 | function AddCupForm({ setCreating, addCup }) {
11 | const [loading, setLoading] = useState(false);
12 | const [error, setError] = useState(null);
13 |
14 | const onSubmit = (cup) => {
15 | setLoading(true);
16 |
17 | create(cup)
18 | .then((res) => {
19 | addCup(res.data);
20 | setCreating(false);
21 | })
22 | .catch((err) => {
23 | setError(err.response.data);
24 | setLoading(false);
25 | });
26 | };
27 |
28 | return (
29 |
72 | );
73 | }
74 |
75 | export default AddCupForm;
76 |
--------------------------------------------------------------------------------