├── .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 |
4 |
5 |
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 |
12 | 13 | {children} 14 | 15 |
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 |