diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 0000000000000000000000000000000000000000..f8b4888565caadc7510be75682268d6c18edd6de --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": null +} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 0000000000000000000000000000000000000000..6b6114114f4e89a1f2d0e911ff090e541af1260c --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,6 @@ +{ + "ExpandedNodes": [ + "" + ], + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..afaa4ec76648db5061c471a079f8186519d4591d Binary files /dev/null and b/.vs/slnx.sqlite differ diff --git a/.vs/teknikattan-scoring-system/v16/.suo b/.vs/teknikattan-scoring-system/v16/.suo new file mode 100644 index 0000000000000000000000000000000000000000..3c47ae39e5f87ee58879d61a5190ed0c9e454669 Binary files /dev/null and b/.vs/teknikattan-scoring-system/v16/.suo differ diff --git a/client/.eslintrc b/client/.eslintrc index 3e2dbeb0fc5a04a61ba058eb0b1015fcc43e9a50..d5bbb2edee2b3642f71bc90f8261324605b0a22e 100644 --- a/client/.eslintrc +++ b/client/.eslintrc @@ -1,29 +1,29 @@ { - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module", - "project": [ - "tsconfig.json" - ] - }, - "ecmaFeatures": { - "jsx": true - }, - "settings": { - "react": { - "version": "detect" - } - }, - "extends": [ - "plugin:react/recommended", - "plugin:@typescript-eslint/recommended", - "prettier/@typescript-eslint", - "plugin:prettier/recommended" - ], - "rules": { - "prettier/prettier": ["warn", { - "endOfLine":"auto" - }] + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module", + "project": ["tsconfig.json"] + }, + "ecmaFeatures": { + "jsx": true + }, + "settings": { + "react": { + "version": "detect" } + }, + "extends": [ + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "prettier/@typescript-eslint", + "plugin:prettier/recommended" + ], + "rules": { + "prettier/prettier": [ + "warn", + { + "endOfLine": "auto" + } + ] } - +} diff --git a/client/package-lock.json b/client/package-lock.json index 133d1ca28e47a3f2e5349f0df025051540c4f6b8..b08413fa1ea4936c7eef606822511df8882cdf81 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2262,6 +2262,15 @@ "@babel/runtime": "^7.12.5" } }, + "@tinymce/tinymce-react": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-3.12.2.tgz", + "integrity": "sha512-M6YQ9e+9rpxrZDOeNPmjgroEfooEKiMVuI4I3+xQtMX1hQQ/t9pGE9nUdS0faZDvqhlc8B/w12GJQNU5ekPo4g==", + "requires": { + "prop-types": "^15.6.2", + "tinymce": "^5.7.1" + } + }, "@types/anymatch": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", @@ -4635,6 +4644,11 @@ } } }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -7211,6 +7225,11 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "fast-memoize": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", + "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==" + }, "fastq": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.1.tgz", @@ -13382,6 +13401,14 @@ } } }, + "re-resizable": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.0.tgz", + "integrity": "sha512-3cUDG81ylyqI0Pdgle/RHwwRYq0ORZzsUaySOCO8IbEtNyaRtrIHYm/jMQ5pjcNiKCxR3vsSymIQZHwJq4gg2Q==", + "requires": { + "fast-memoize": "^2.5.1" + } + }, "react": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", @@ -13525,6 +13552,15 @@ "scheduler": "^0.20.1" } }, + "react-draggable": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz", + "integrity": "sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==", + "requires": { + "classnames": "^2.2.5", + "prop-types": "^15.6.0" + } + }, "react-error-overlay": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", @@ -13557,6 +13593,23 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==" }, + "react-rnd": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.2.4.tgz", + "integrity": "sha512-wseACIsxa1wuZz9XatO3/JAZR748Sddehh0NtJz1Yj3X5BQm5pwRShiadfnWrUajJATurHbN0NVTUn+jEkHkPw==", + "requires": { + "re-resizable": "6.9.0", + "react-draggable": "4.4.3", + "tslib": "2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, "react-router": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", @@ -15912,6 +15965,11 @@ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, + "tinymce": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-5.7.1.tgz", + "integrity": "sha512-1gY8RClc734srSlkYwY0MQzmkS1j73PuPC+nYtNtrrQVPY9VNcZ4bOiRwzTbdjPPD8GOtv6BAk8Ww/H2RiqKpA==" + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", diff --git a/client/package.json b/client/package.json index 6e4d3573da8b17aa5c8282606b69de472dbe8887..f41350a6740cde52afbbaa3d6de2cf77ac48d032 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,7 @@ "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.6.3", + "@tinymce/tinymce-react": "^3.12.2", "@types/jest": "^26.0.20", "@types/node": "^12.19.16", "@types/react": "^17.0.1", @@ -21,6 +22,7 @@ "react-axios": "^2.0.4", "react-dom": "^17.0.1", "react-redux": "^7.2.2", + "react-rnd": "^10.2.4", "react-router-dom": "^5.2.0", "react-scripts": "4.0.2", "redux": "^4.0.5", diff --git a/client/public/favicon.ico b/client/public/favicon.ico index a11777cc471a4344702741ab1c8a588998b1311a..f3dde5b1d1464d1c26312a8f28bd829a2604b731 100644 Binary files a/client/public/favicon.ico and b/client/public/favicon.ico differ diff --git a/client/public/index.html b/client/public/index.html index aa069f27cbd9d53394428171c3989fd03db73c76..c3e215c0d247809d37fc2a433e8ba54c2f861181 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -5,10 +5,7 @@ <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> - <meta - name="description" - content="Web site created using create-react-app" - /> + <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <!-- manifest.json provides metadata used when your web app is installed on a @@ -24,7 +21,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - <title>React App</title> + <title>Teknikåttan</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> diff --git a/client/src/Main.tsx b/client/src/Main.tsx index c5494c888b84e684299abfba1fdf7d087fc9c924..f32aad9fbf044a2fd055fa29e30e0ea830898e5b 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -6,6 +6,7 @@ import PresentationEditorPage from './pages/presentationEditor/PresentationEdito import AudienceViewPage from './pages/views/AudienceViewPage' import JudgeViewPage from './pages/views/JudgeViewPage' import ParticipantViewPage from './pages/views/ParticipantViewPage' +import PresenterViewPage from './pages/views/PresenterViewPage' import ViewSelectPage from './pages/views/ViewSelectPage' import SecureRoute from './utils/SecureRoute' @@ -17,9 +18,10 @@ const Main: React.FC = () => { <SecureRoute path="/admin" component={AdminPage} /> <SecureRoute path="/editor/competition-id=:id" component={PresentationEditorPage} /> <Route exact path="/view" component={ViewSelectPage} /> - <Route exact path="/view/participant" component={ParticipantViewPage} /> - <Route exact path="/view/judge" component={JudgeViewPage} /> - <Route exact path="/view/audience" component={AudienceViewPage} /> + <Route exact path="/participant/id=:id&code=:code" component={ParticipantViewPage} /> + <SecureRoute exact path="/presenter/id=:id&code=:code" component={PresenterViewPage} /> + <Route exact path="/judge/id=:id&code=:code" component={JudgeViewPage} /> + <Route exact path="/audience/id=:id&code=:code" component={AudienceViewPage} /> </Switch> </BrowserRouter> ) diff --git a/client/src/actions/competitions.ts b/client/src/actions/competitions.ts index a758fd635550306eecb09e8100d5e17cb09a8ce1..7d248e46c05df64e948b483c5092616c4a828f35 100644 --- a/client/src/actions/competitions.ts +++ b/client/src/actions/competitions.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import { CompetitionFilterParams } from '../interfaces/CompetitionFilterParams' +import { CompetitionFilterParams } from '../interfaces/FilterParams' import { AppDispatch, RootState } from './../store' import Types from './types' diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e482d2b4c51d49fdd04dd8f18721a1e74e82d5a --- /dev/null +++ b/client/src/actions/presentation.ts @@ -0,0 +1,44 @@ +import axios from 'axios' +import { Slide } from '../interfaces/Slide' +import { AppDispatch } from './../store' +import Types from './types' + +export const getPresentationCompetition = (id: string) => async (dispatch: AppDispatch) => { + await axios + .get(`/competitions/${id}`) + .then((res) => { + dispatch({ + type: Types.SET_PRESENTATION_COMPETITION, + payload: res.data, + }) + }) + .catch((err) => { + console.log(err) + }) +} + +export const getPresentationTeams = (id: string) => async (dispatch: AppDispatch) => { + await axios + .get(`/competitions/${id}/teams`) + .then((res) => { + dispatch({ + type: Types.SET_PRESENTATION_TEAMS, + payload: res.data.items, + }) + }) + .catch((err) => { + console.log(err) + }) +} + +export const setCurrentSlide = (slide: Slide) => (dispatch: AppDispatch) => { + dispatch({ type: Types.SET_PRESENTATION_SLIDE, payload: slide }) +} + +export const setCurrentSlidePrevious = () => (dispatch: AppDispatch) => { + dispatch({ type: Types.SET_PRESENTATION_SLIDE_PREVIOUS }) +} + +export const setCurrentSlideNext = () => (dispatch: AppDispatch) => { + dispatch({ type: Types.SET_PRESENTATION_SLIDE_NEXT }) +} diff --git a/client/src/actions/searchUser.ts b/client/src/actions/searchUser.ts index fb4bf122e40d13952537c3e942bf351f10f99604..c6211056bda8250f1b5c849450d018ef4bd42819 100644 --- a/client/src/actions/searchUser.ts +++ b/client/src/actions/searchUser.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import { UserFilterParams } from '../interfaces/UserData' +import { UserFilterParams } from '../interfaces/FilterParams' import { AppDispatch, RootState } from './../store' import Types from './types' diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index b72a35862f4c42161a25997a6065662d1020e8f1..c0b7b629afa54a43de4c8ef8ddfba2cc551aa5d4 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -15,6 +15,11 @@ export default { SET_COMPETITIONS_FILTER_PARAMS: 'SET_COMPETITIONS_FILTER_PARAMS', SET_COMPETITIONS_TOTAL: 'SET_COMPETITIONS_TOTAL', SET_COMPETITIONS_COUNT: 'SET_COMPETITIONS_COUNT', + SET_PRESENTATION_COMPETITION: 'SET_PRESENTATION_COMPETITION', + SET_PRESENTATION_SLIDE: 'SET_PRESENTATION_SLIDE', + SET_PRESENTATION_SLIDE_PREVIOUS: 'SET_PRESENTATION_SLIDE_PREVIOUS', + SET_PRESENTATION_SLIDE_NEXT: 'SET_PRESENTATION_SLIDE_NEXT', + SET_PRESENTATION_TEAMS: 'SET_PRESENTATION_TEAMS', SET_CITIES: 'SET_CITIES', SET_CITIES_TOTAL: 'SET_CITIES_TOTAL', SET_CITIES_COUNT: 'SET_CITIES_COUNT', diff --git a/client/src/actions/user.ts b/client/src/actions/user.ts index b543a68ae0631036e3a2df1f1312698c733f5bfa..72d764c861db49024c5fbcf00e3291efc35b2609 100644 --- a/client/src/actions/user.ts +++ b/client/src/actions/user.ts @@ -1,10 +1,10 @@ import axios from 'axios' import { History } from 'history' import { AppDispatch } from '../store' -import { AdminLoginData } from './../interfaces/AdminLoginData' +import { AccountLoginModel } from './../interfaces/FormModels' import Types from './types' -export const loginUser = (userData: AdminLoginData, history: History) => async (dispatch: AppDispatch) => { +export const loginUser = (userData: AccountLoginModel, history: History) => async (dispatch: AppDispatch) => { dispatch({ type: Types.LOADING_UI }) await axios .post('/auth/login', userData) diff --git a/client/src/enum/ComponentTypes.ts b/client/src/enum/ComponentTypes.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8d194c09ebf38dd98e6a49a7062345cd110620a --- /dev/null +++ b/client/src/enum/ComponentTypes.ts @@ -0,0 +1,5 @@ +export enum ComponentTypes { + Text, + Checkbox, + Image, +} diff --git a/client/src/index.css b/client/src/index.css index 80bfba852206d422d31dd7fbce7c438ef107f361..6b920fab9162797dfd4399edfa866c0b71708f51 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -7,6 +7,15 @@ body { -moz-osx-font-smoothing: grayscale; } +.tox-edit-area__iframe, +.tox-edit-area { + background: transparent !important; +} + +.tox-notifications-container { + display: none; +} + html, #root { height: 100%; diff --git a/client/src/interfaces/AdminLoginData.ts b/client/src/interfaces/AdminLoginData.ts deleted file mode 100644 index 70f61036d73c599f650abcca254c3ba5c6594992..0000000000000000000000000000000000000000 --- a/client/src/interfaces/AdminLoginData.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface AdminLoginData { - email: string - password: string -} diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts new file mode 100644 index 0000000000000000000000000000000000000000..194fb2e727449b39befe2e1d43ee9913cca1ee00 --- /dev/null +++ b/client/src/interfaces/ApiModels.ts @@ -0,0 +1,74 @@ +interface NameID { + id: number + name: string +} +export interface City extends NameID {} +export interface Role extends NameID {} +export interface MediaType extends NameID {} +export interface QuestionType extends NameID {} + +export interface Media { + id: number + filename: string + mediatype_id: number + user_id: number +} + +export interface User extends NameID { + email: string + role_id: number + city_id: number +} + +export interface Competition extends NameID { + city_id: number + year: number +} + +export interface Team extends NameID { + competition_id: number +} + +export interface Question extends NameID { + slide_id: number + title: string + total_score: number + type_id: number +} + +export interface QuestionAlternative { + id: number + text: string + value: boolean + question_id: number +} +export interface QuestionAnswer { + id: number + question_id: number + team_id: string + data: string + score: number +} + +export interface Component { + id: number + x: number + y: number + w: number + h: number + type: number +} + +export interface ImageComponent extends Component { + media_id: number +} + +export interface TextComponent extends Component { + text: string + font: string +} + +export interface QuestionAlternativeComponent extends Component { + question_alternative_id: number + font: string +} diff --git a/client/src/interfaces/ApiRichModels.ts b/client/src/interfaces/ApiRichModels.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f7e012435518939f79c9bc30d43e17458215026 --- /dev/null +++ b/client/src/interfaces/ApiRichModels.ts @@ -0,0 +1,37 @@ +import { City, Component, Media, QuestionAnswer, QuestionType } from './ApiModels' + +export interface RichCompetition { + name: string + id: number + year: number + city: City + slides: RichSlide[] + teams: RichTeam[] +} + +export interface RichSlide { + id: number + order: number + timer: number + title: string + competition_id: number + question: RichQuestion[] + components: Component[] + medias: Media[] +} + +export interface RichTeam { + id: number + name: string + question_answers: QuestionAnswer[] + competition_id: number +} + +export interface RichQuestion { + id: number + slide_id: number + name: string + title: string + total_score: number + question_type: QuestionType +} diff --git a/client/src/interfaces/Competition.ts b/client/src/interfaces/Competition.ts deleted file mode 100644 index d36d97da1bc824f996fb7c46e88eaa71aa720995..0000000000000000000000000000000000000000 --- a/client/src/interfaces/Competition.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Competition { - name: string - id: number - city_id: number - year: number -} diff --git a/client/src/interfaces/CompetitionFilterParams.ts b/client/src/interfaces/CompetitionFilterParams.ts deleted file mode 100644 index 839460527a548572cf4dc26015f477e33e871187..0000000000000000000000000000000000000000 --- a/client/src/interfaces/CompetitionFilterParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface CompetitionFilterParams { - name?: string - year?: number - cityId?: number - styleId?: number - page: number - pageSize: number -} diff --git a/client/src/interfaces/Components.ts b/client/src/interfaces/Components.ts new file mode 100644 index 0000000000000000000000000000000000000000..864b987d40772e25640cad0029914f130081cf6b --- /dev/null +++ b/client/src/interfaces/Components.ts @@ -0,0 +1,9 @@ +export interface Position { + x: number + y: number +} + +export interface Size { + w: number + h: number +} diff --git a/client/src/interfaces/FilterParams.ts b/client/src/interfaces/FilterParams.ts new file mode 100644 index 0000000000000000000000000000000000000000..eca6551f4705aa3e4dfe6c96878fc9d3039fd983 --- /dev/null +++ b/client/src/interfaces/FilterParams.ts @@ -0,0 +1,26 @@ +export interface CompetitionFilterParams { + name?: string + year?: number + cityId?: number + styleId?: number + page: number + pageSize: number +} + +export interface SearchUserFilterParams { + name?: string + year?: number + cityId?: number + styleId?: number + page: number + pageSize: number +} + +export interface UserFilterParams { + name?: string + email?: string + cityId?: number + roleId?: number + page: number + pageSize: number +} diff --git a/client/src/interfaces/models.ts b/client/src/interfaces/FormModels.ts similarity index 62% rename from client/src/interfaces/models.ts rename to client/src/interfaces/FormModels.ts index 70d4cc08b5f8e449ad251526389dd84fe42a4d3e..dd51f32b18fb75ba5c7c3c535e7f8d2328f9bb2d 100644 --- a/client/src/interfaces/models.ts +++ b/client/src/interfaces/FormModels.ts @@ -1,18 +1,31 @@ +export interface ServerResponse { + code: number + message: string +} +export interface FormModel<T> { + model: T + error?: string +} + +//#region LOGIN export interface AccountLoginModel { email: string password: string } +export interface CompetitionLoginModel { + code: string +} + +//#endregion + +////ADD//// export interface AddCompetitionModel { name: string city: string year: number } -export interface CompetitionLoginModel { - code: string -} - export interface AddUserModel { email: string password: string @@ -21,9 +34,15 @@ export interface AddUserModel { name?: string } +export interface AddCityModel { + name: string +} + +////EDIT//// export interface EditUserModel { email: string role: string city: string name?: string + password?: string } diff --git a/client/src/interfaces/Role.ts b/client/src/interfaces/Role.ts deleted file mode 100644 index 08a54dfcb012ee03398d17d4a7153ab040cde909..0000000000000000000000000000000000000000 --- a/client/src/interfaces/Role.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Role { - id: number - name: string -} diff --git a/client/src/interfaces/SearchUserFilterParams.ts b/client/src/interfaces/SearchUserFilterParams.ts deleted file mode 100644 index 8d7230d83d12d164a0db4683b4db7b791d835a81..0000000000000000000000000000000000000000 --- a/client/src/interfaces/SearchUserFilterParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface SearchUSerFilterParams { - name?: string - year?: number - cityId?: number - styleId?: number - page: number - pageSize: number -} diff --git a/client/src/interfaces/Slide.ts b/client/src/interfaces/Slide.ts new file mode 100644 index 0000000000000000000000000000000000000000..dcdbd366cffa4482c9de7979663c3750bcad2f4d --- /dev/null +++ b/client/src/interfaces/Slide.ts @@ -0,0 +1,7 @@ +export interface Slide { + competition_id: number + id: number + order: number + timer: number + title: string +} diff --git a/client/src/interfaces/City.ts b/client/src/interfaces/Team.ts similarity index 55% rename from client/src/interfaces/City.ts rename to client/src/interfaces/Team.ts index e5190b6b56284e97015a66b1c2febd0548bd236a..10b4350e5c2ffc6068308bd90e12adfaf584ef24 100644 --- a/client/src/interfaces/City.ts +++ b/client/src/interfaces/Team.ts @@ -1,4 +1,4 @@ -export interface City { +export interface Team { id: number name: string } diff --git a/client/src/interfaces/UserData.ts b/client/src/interfaces/UserData.ts deleted file mode 100644 index a0c7394699b314c49e8f19bb259b6d73dd610bcf..0000000000000000000000000000000000000000 --- a/client/src/interfaces/UserData.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface UserData { - id: number - name?: string - email: string - role_id: number - city_id: number -} - -export interface UserFilterParams { - name?: string - email?: string - cityId?: number - roleId?: number - page: number - pageSize: number -} diff --git a/client/src/interfaces/ViewParams.ts b/client/src/interfaces/ViewParams.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9aa6a5c5f81a6bf852f8caa30443a793b0dddd7 --- /dev/null +++ b/client/src/interfaces/ViewParams.ts @@ -0,0 +1,4 @@ +export interface ViewParams { + id: string + code: string +} diff --git a/client/src/pages/admin/AdminPage.test.tsx b/client/src/pages/admin/AdminPage.test.tsx index efb56bb24b9ff8726e5a402816b3440f8a6107e6..ab694cee23501ef51a644c1df30f36662d9d36f6 100644 --- a/client/src/pages/admin/AdminPage.test.tsx +++ b/client/src/pages/admin/AdminPage.test.tsx @@ -1,4 +1,5 @@ import { render } from '@testing-library/react' +import mockedAxios from 'axios' import React from 'react' import { Provider } from 'react-redux' import { BrowserRouter } from 'react-router-dom' @@ -6,6 +7,42 @@ import store from '../../store' import AdminPage from './AdminPage' it('renders admin view', () => { + const cityRes: any = { + data: { + items: [ + { + id: 1, + name: 'Link\u00f6ping', + }, + { + id: 2, + name: 'Stockholm', + }, + ], + count: 2, + total_count: 3, + }, + } + const rolesRes: any = { + data: { + items: [ + { + id: 1, + name: 'role1', + }, + { + id: 2, + name: 'role2', + }, + ], + count: 2, + total_count: 3, + }, + } + ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => { + if (path === '/misc/cities') return Promise.resolve(cityRes) + else return Promise.resolve(rolesRes) + }) render( <Provider store={store}> <BrowserRouter> diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index 270e2dd0c8c3ec20a57fbe628605b8ecf574d205..921f721db03d22ee9e7d010cb67620ede98e4bd2 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -12,18 +12,21 @@ import { } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import DashboardIcon from '@material-ui/icons/Dashboard' -import MailIcon from '@material-ui/icons/Mail' -import React from 'react' +import ExitToAppIcon from '@material-ui/icons/ExitToApp' +import LocationCityIcon from '@material-ui/icons/LocationCity' +import PeopleIcon from '@material-ui/icons/People' +import SettingsOverscanIcon from '@material-ui/icons/SettingsOverscan' +import React, { useEffect } from 'react' import { Link, Route, Switch, useRouteMatch } from 'react-router-dom' +import { getCities } from '../../actions/cities' +import { getRoles } from '../../actions/roles' import { logoutUser } from '../../actions/user' -import { useAppDispatch } from '../../hooks' -import CompetitionManager from './components/CompetitionManager' -import Regions from './components/Regions' -import UserManager from './components/UserManager' +import { useAppDispatch, useAppSelector } from '../../hooks' +import CompetitionManager from './competitions/CompetitionManager' +import RegionManager from './regions/Regions' import { LeftDrawer } from './styled' - +import UserManager from './users/UserManager' const drawerWidth = 250 -const menuItems = ['Startsida', 'Regioner', 'Användare', 'Tävlingshanterare'] const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -42,7 +45,7 @@ const useStyles = makeStyles((theme: Theme) => content: { flexGrow: 1, backgroundColor: theme.palette.background.default, - paddingLeft: theme.spacing(30), + paddingLeft: theme.spacing(31), }, }) ) @@ -55,13 +58,50 @@ const AdminView: React.FC = () => { dispatch(logoutUser()) } const dispatch = useAppDispatch() + const currentUser = useAppSelector((state) => state.user.userInfo) + const isAdmin = () => currentUser && currentUser.role.name === 'Admin' + + const menuAdminItems = [ + { text: 'Startsida', icon: DashboardIcon }, + { text: 'Regioner', icon: LocationCityIcon }, + { text: 'Användare', icon: PeopleIcon }, + { text: 'Tävlingshanterare', icon: SettingsOverscanIcon }, + ] + + const menuEditorItems = [ + { text: 'Startsida', icon: DashboardIcon }, + { text: 'Tävlingshanterare', icon: SettingsOverscanIcon }, + ] + + const renderItems = () => { + const menuItems = isAdmin() ? menuAdminItems : menuEditorItems + return menuItems.map((value, index) => ( + <ListItem + button + component={Link} + key={value.text} + to={`${url}/${value.text.toLowerCase()}`} + selected={index === openIndex} + onClick={() => setOpenIndex(index)} + > + <ListItemIcon>{React.createElement(value.icon)}</ListItemIcon> + <ListItemText primary={value.text} /> + </ListItem> + )) + } + + useEffect(() => { + dispatch(getCities()) + dispatch(getRoles()) + }, []) + return ( <div className={classes.root}> <CssBaseline /> <AppBar position="fixed" className={classes.appBar}> <Toolbar> <Typography variant="h5" noWrap> - {menuItems[openIndex]} + {isAdmin() ? menuAdminItems[openIndex].text : menuEditorItems[openIndex].text} </Typography> </Toolbar> </AppBar> @@ -76,25 +116,18 @@ const AdminView: React.FC = () => { <div> <div className={classes.toolbar} /> <Divider /> - <List> - {menuItems.map((text, index) => ( - <ListItem - button - component={Link} - key={text} - to={`${url}/${text.toLowerCase()}`} - selected={index === openIndex} - onClick={() => setOpenIndex(index)} - > - <ListItemIcon>{index === 0 ? <DashboardIcon /> : <MailIcon />}</ListItemIcon> - <ListItemText primary={text} /> - </ListItem> - ))} - </List> + <List>{renderItems()}</List> <Divider /> <List> <ListItem> - <Button onClick={handleLogout} type="submit" fullWidth variant="contained" color="primary"> + <Button + onClick={handleLogout} + type="submit" + fullWidth + variant="contained" + color="primary" + endIcon={<ExitToAppIcon></ExitToAppIcon>} + > Logga ut </Button> </ListItem> @@ -110,7 +143,7 @@ const AdminView: React.FC = () => { </Typography> </Route> <Route path={`${path}/regioner`}> - <Regions /> + <RegionManager /> </Route> <Route path={`${path}/användare`}> <UserManager /> diff --git a/client/src/pages/admin/components/AddCompetition.test.tsx b/client/src/pages/admin/competitions/AddCompetition.test.tsx similarity index 100% rename from client/src/pages/admin/components/AddCompetition.test.tsx rename to client/src/pages/admin/competitions/AddCompetition.test.tsx diff --git a/client/src/pages/admin/components/AddCompetition.tsx b/client/src/pages/admin/competitions/AddCompetition.tsx similarity index 85% rename from client/src/pages/admin/components/AddCompetition.tsx rename to client/src/pages/admin/competitions/AddCompetition.tsx index b67714babe4c1d57040684f1bcc1e3cfabd82836..3d45e629769c642cde13499ba988901a6c71a0d9 100644 --- a/client/src/pages/admin/components/AddCompetition.tsx +++ b/client/src/pages/admin/competitions/AddCompetition.tsx @@ -6,22 +6,15 @@ import React from 'react' import * as Yup from 'yup' import { getCompetitions } from '../../../actions/competitions' import { useAppDispatch, useAppSelector } from '../../../hooks' -import { City } from '../../../interfaces/City' -import { AddCompetitionModel } from '../../../interfaces/models' -import { AddCompetitionButton, AddCompetitionContent, AddCompetitionForm } from './styled' +import { City } from '../../../interfaces/ApiModels' +import { AddCompetitionModel, FormModel } from '../../../interfaces/FormModels' +import { AddButton, AddContent, AddForm } from '../styledComp' -interface ServerResponse { - code: number - message: string -} +type formType = FormModel<AddCompetitionModel> -interface AddCompetitionFormModel { - model: AddCompetitionModel - error?: string -} const noCitySelected = 'Välj stad' -const competitionSchema: Yup.SchemaOf<AddCompetitionFormModel> = Yup.object({ +const competitionSchema: Yup.SchemaOf<formType> = Yup.object({ model: Yup.object() .shape({ name: Yup.string().required('Namn krävs'), @@ -51,17 +44,14 @@ const AddCompetition: React.FC = (props: any) => { const dispatch = useAppDispatch() const id = open ? 'simple-popover' : undefined const currentYear = new Date().getFullYear() - const handleCompetitionSubmit = async ( - values: AddCompetitionFormModel, - actions: FormikHelpers<AddCompetitionFormModel> - ) => { + const handleCompetitionSubmit = async (values: formType, actions: FormikHelpers<formType>) => { const params = { name: values.model.name, year: values.model.year, city_id: selectedCity?.id as number, } await axios - .post<ServerResponse>('/competitions', params) + .post('/competitions', params) .then(() => { actions.resetForm() setAnchorEl(null) @@ -79,14 +69,19 @@ const AddCompetition: React.FC = (props: any) => { }) } - const competitionInitialValues: AddCompetitionFormModel = { + const competitionInitialValues: formType = { model: { name: '', city: userCity?.name ? userCity.name : noCitySelected, year: currentYear }, } return ( <div> - <AddCompetitionButton color="secondary" variant="contained" onClick={handleClick}> + <AddButton + style={{ backgroundColor: '#4caf50', color: '#fcfcfc' }} + color="default" + variant="contained" + onClick={handleClick} + > Ny Tävling - </AddCompetitionButton> + </AddButton> <Popover id={id} open={open} @@ -101,14 +96,14 @@ const AddCompetition: React.FC = (props: any) => { horizontal: 'center', }} > - <AddCompetitionContent> + <AddContent> <Formik initialValues={competitionInitialValues} validationSchema={competitionSchema} onSubmit={handleCompetitionSubmit} > {(formik) => ( - <AddCompetitionForm onSubmit={formik.handleSubmit}> + <AddForm onSubmit={formik.handleSubmit}> <TextField label="Namn" name="model.name" @@ -170,10 +165,10 @@ const AddCompetition: React.FC = (props: any) => { {formik.errors.error} </Alert> )} - </AddCompetitionForm> + </AddForm> )} </Formik> - </AddCompetitionContent> + </AddContent> </Popover> </div> ) diff --git a/client/src/pages/admin/components/CompetitionManager.test.tsx b/client/src/pages/admin/competitions/CompetitionManager.test.tsx similarity index 100% rename from client/src/pages/admin/components/CompetitionManager.test.tsx rename to client/src/pages/admin/competitions/CompetitionManager.test.tsx diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/competitions/CompetitionManager.tsx similarity index 93% rename from client/src/pages/admin/components/CompetitionManager.tsx rename to client/src/pages/admin/competitions/CompetitionManager.tsx index 2acce64b8b11b33e089f15f8d34b1472de2b8898..08a18581b05e8015c4743fcc75655847bb987c7c 100644 --- a/client/src/pages/admin/components/CompetitionManager.tsx +++ b/client/src/pages/admin/competitions/CompetitionManager.tsx @@ -14,13 +14,12 @@ import TableRow from '@material-ui/core/TableRow' import MoreHorizIcon from '@material-ui/icons/MoreHoriz' import axios from 'axios' import React, { useEffect } from 'react' -import { Link } from 'react-router-dom' -import { getCities } from '../../../actions/cities' +import { Link, useHistory } from 'react-router-dom' import { getCompetitions, setFilterParams } from '../../../actions/competitions' import { useAppDispatch, useAppSelector } from '../../../hooks' -import { CompetitionFilterParams } from '../../../interfaces/CompetitionFilterParams' +import { CompetitionFilterParams } from '../../../interfaces/FilterParams' +import { FilterContainer, RemoveMenuItem, TopBar, YearFilterTextField } from '../styledComp' import AddCompetition from './AddCompetition' -import { FilterContainer, RemoveCompetition, TopBar, YearFilterTextField } from './styled' const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -44,6 +43,7 @@ const CompetitionManager: React.FC = (props: any) => { const classes = useStyles() const noFilterText = 'Alla' const dispatch = useAppDispatch() + const history = useHistory() const handleClick = (event: React.MouseEvent<HTMLButtonElement>, id: number) => { setAnchorEl(event.currentTarget) setActiveId(id) @@ -55,7 +55,6 @@ const CompetitionManager: React.FC = (props: any) => { } useEffect(() => { - dispatch(getCities()) dispatch(getCompetitions()) }, []) @@ -176,9 +175,9 @@ const CompetitionManager: React.FC = (props: any) => { onChangePage={(event, newPage) => handleFilterChange({ ...filterParams, page: newPage })} /> <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> - <MenuItem onClick={handleClose}>Starta</MenuItem> + <MenuItem onClick={() => history.push(`/presenter/id=${activeId}&code=123123`)}>Starta</MenuItem> <MenuItem onClick={handleClose}>Duplicera</MenuItem> - <RemoveCompetition onClick={handleDeleteCompetition}>Ta bort</RemoveCompetition> + <RemoveMenuItem onClick={handleDeleteCompetition}>Ta bort</RemoveMenuItem> </Menu> </div> ) diff --git a/client/src/pages/admin/regions/AddRegion.tsx b/client/src/pages/admin/regions/AddRegion.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c8ae412e680bda652327193af44cb728e36c1621 --- /dev/null +++ b/client/src/pages/admin/regions/AddRegion.tsx @@ -0,0 +1,112 @@ +import { Grid, TextField } from '@material-ui/core' +import FormControl from '@material-ui/core/FormControl' +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' +import AddIcon from '@material-ui/icons/Add' +import { Alert, AlertTitle } from '@material-ui/lab' +import axios from 'axios' +import { Formik, FormikHelpers } from 'formik' +import React from 'react' +import * as Yup from 'yup' +import { getCities } from '../../../actions/cities' +import { useAppDispatch } from '../../../hooks' +import { AddCityModel, FormModel } from '../../../interfaces/FormModels' +import { AddButton, AddForm } from '../styledComp' + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + table: { + width: '100%', + }, + margin: { + margin: theme.spacing(1), + }, + button: { + width: '40px', + height: '40px', + marginTop: '20px', + }, + }) +) + +type formType = FormModel<AddCityModel> + +const schema: Yup.SchemaOf<formType> = Yup.object({ + model: Yup.object() + .shape({ + name: Yup.string() + .required('Minst två bokstäver krävs') + .min(2) + .matches(/[a-zA-Z]/, 'Namnet får enbart innehålla a-z, A-Z.'), + }) + .required(), + error: Yup.string().optional(), +}) + +const AddRegion: React.FC = (props: any) => { + const classes = useStyles() + const dispatch = useAppDispatch() + + const handleSubmit = async (values: formType, actions: FormikHelpers<formType>) => { + const params = { + name: values.model.name, + } + await axios + .post('/misc/cities', params) + .then(() => { + actions.resetForm() + dispatch(getCities()) + }) + .catch(({ response }) => { + console.warn(response.data) + if (response.data && response.data.message) + actions.setFieldError('error', response.data && response.data.message) + else actions.setFieldError('error', 'Something went wrong, please try again') + }) + .finally(() => { + actions.setSubmitting(false) + }) + } + + const initValues: formType = { + model: { name: '' }, + } + + return ( + <Formik initialValues={initValues} validationSchema={schema} onSubmit={handleSubmit}> + {(formik) => ( + <AddForm onSubmit={formik.handleSubmit}> + <FormControl className={classes.margin}> + <Grid container={true}> + <TextField + className={classes.margin} + helperText={formik.touched.model?.name ? formik.errors.model?.name : ''} + error={Boolean(formik.touched.model?.name && formik.errors.model?.name)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + name="model.name" + label="Region" + ></TextField> + <AddButton + style={{ backgroundColor: '#4caf50', color: '#fcfcfc' }} + className={classes.button} + color="default" + variant="contained" + type="submit" + > + <AddIcon></AddIcon> + </AddButton> + </Grid> + </FormControl> + {formik.errors.error && ( + <Alert severity="error"> + <AlertTitle>Error</AlertTitle> + {formik.errors.error} + </Alert> + )} + </AddForm> + )} + </Formik> + ) +} + +export default AddRegion diff --git a/client/src/pages/admin/components/Regions.tsx b/client/src/pages/admin/regions/Regions.tsx similarity index 82% rename from client/src/pages/admin/components/Regions.tsx rename to client/src/pages/admin/regions/Regions.tsx index 103c2b1d820643a3fe80b2a2e241e42505eb6e5d..eb79575ea7857658742f30f02d5f96a5e87e7f6d 100644 --- a/client/src/pages/admin/components/Regions.tsx +++ b/client/src/pages/admin/regions/Regions.tsx @@ -1,5 +1,4 @@ -import { Button, Menu, TextField, Typography } from '@material-ui/core' -import FormControl from '@material-ui/core/FormControl' +import { Button, Menu, Typography } from '@material-ui/core' import Paper from '@material-ui/core/Paper' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import Table from '@material-ui/core/Table' @@ -13,8 +12,8 @@ import axios from 'axios' import React, { useEffect } from 'react' import { getCities } from '../../../actions/cities' import { useAppDispatch, useAppSelector } from '../../../hooks' -import { RemoveCompetition, TopBar } from './styled' - +import { RemoveMenuItem, TopBar } from '../styledComp' +import AddRegion from './AddRegion' const useStyles = makeStyles((theme: Theme) => createStyles({ table: { @@ -26,12 +25,12 @@ const useStyles = makeStyles((theme: Theme) => }) ) -const UserManager: React.FC = (props: any) => { +const RegionManager: React.FC = (props: any) => { const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) const [activeId, setActiveId] = React.useState<number | undefined>(undefined) const citiesTotal = useAppSelector((state) => state.cities.total) const cities = useAppSelector((state) => state.cities.cities) - const [newCity, setNewCity] = React.useState() + const [newCity, setNewCity] = React.useState<string>() const classes = useStyles() const dispatch = useAppDispatch() const handleClose = () => { @@ -81,12 +80,7 @@ const UserManager: React.FC = (props: any) => { return ( <div> <TopBar> - <FormControl className={classes.margin}> - <TextField className={classes.margin} value={newCity} onChange={handleChange} label="Region"></TextField> - <Button color="primary" variant="contained" onClick={handleAddCity}> - Lägg till - </Button> - </FormControl> + <AddRegion></AddRegion> </TopBar> <TableContainer component={Paper}> <Table className={classes.table} aria-label="simple table"> @@ -113,10 +107,10 @@ const UserManager: React.FC = (props: any) => { {(!cities || cities.length === 0) && <Typography>Inga regioner hittades</Typography>} </TableContainer> <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> - <RemoveCompetition onClick={handleDeleteCity}>Ta bort</RemoveCompetition> + <RemoveMenuItem onClick={handleDeleteCity}>Ta bort</RemoveMenuItem> </Menu> </div> ) } -export default UserManager +export default RegionManager diff --git a/client/src/pages/admin/components/styled.tsx b/client/src/pages/admin/styledComp.tsx similarity index 68% rename from client/src/pages/admin/components/styled.tsx rename to client/src/pages/admin/styledComp.tsx index 69040178507526760405089015c6928652cf7897..228b712e1e8ea10e831b5ac68609d713dc89de68 100644 --- a/client/src/pages/admin/components/styled.tsx +++ b/client/src/pages/admin/styledComp.tsx @@ -7,20 +7,22 @@ export const TopBar = styled.div` align-items: flex-end; ` -export const AddCompetitionButton = styled(Button)` +export const AddButton = styled(Button)` margin-bottom: 8px; ` -export const AddCompetitionForm = styled.form` +export const AddForm = styled.form` display: flex; flex-direction: column; ` -export const AddCompetitionContent = styled.div` - padding: 15px; +export const AddContent = styled.div` + padding: 25px; + padding-bottom: 40px; + width: 300px; ` -export const RemoveCompetition = styled(MenuItem)` +export const RemoveMenuItem = styled(MenuItem)` color: red; ` diff --git a/client/src/pages/admin/components/AddUser.tsx b/client/src/pages/admin/users/AddUser.tsx similarity index 87% rename from client/src/pages/admin/components/AddUser.tsx rename to client/src/pages/admin/users/AddUser.tsx index 81c6adc96b26e230acf1ee2b73b68b28c293d7be..d58a5d5dc153463618015e9c4195958f5160bbcb 100644 --- a/client/src/pages/admin/components/AddUser.tsx +++ b/client/src/pages/admin/users/AddUser.tsx @@ -1,4 +1,5 @@ import { Button, FormControl, InputLabel, MenuItem, Popover, TextField } from '@material-ui/core' +import PersonAddIcon from '@material-ui/icons/PersonAdd' import { Alert, AlertTitle } from '@material-ui/lab' import axios from 'axios' import { Formik, FormikHelpers } from 'formik' @@ -6,28 +7,19 @@ import React from 'react' import * as Yup from 'yup' import { getSearchUsers } from '../../../actions/searchUser' import { useAppDispatch, useAppSelector } from '../../../hooks' -import { City } from '../../../interfaces/City' -import { AddUserModel } from '../../../interfaces/models' -import { Role } from '../../../interfaces/Role' -import { AddCompetitionButton, AddCompetitionContent, AddCompetitionForm } from './styled' +import { City, Role } from '../../../interfaces/ApiModels' +import { AddUserModel, FormModel } from '../../../interfaces/FormModels' +import { AddButton, AddContent, AddForm } from '../styledComp' -interface ServerResponse { - code: number - message: string -} - -interface AddUserFormModel { - model: AddUserModel - error?: string -} +type formType = FormModel<AddUserModel> const noRoleSelected = 'Välj roll' const noCitySelected = 'Välj stad' -const userSchema: Yup.SchemaOf<AddUserFormModel> = Yup.object({ +const userSchema: Yup.SchemaOf<formType> = Yup.object({ model: Yup.object() .shape({ - name: Yup.string(), //.required('Namn krävs'), + name: Yup.string(), email: Yup.string().email().required('Email krävs'), password: Yup.string() .required('Lösenord krävs.') @@ -58,7 +50,7 @@ const AddUser: React.FC = (props: any) => { const open = Boolean(anchorEl) const dispatch = useAppDispatch() const id = open ? 'simple-popover' : undefined - const handleCompetitionSubmit = async (values: AddUserFormModel, actions: FormikHelpers<AddUserFormModel>) => { + const handleSubmit = async (values: formType, actions: FormikHelpers<formType>) => { const params = { email: values.model.email, password: values.model.password, @@ -67,7 +59,7 @@ const AddUser: React.FC = (props: any) => { role_id: selectedRole?.id as number, } await axios - .post<ServerResponse>('/auth/signup', params) + .post('/auth/signup', params) .then(() => { actions.resetForm() setAnchorEl(null) @@ -86,14 +78,20 @@ const AddUser: React.FC = (props: any) => { }) } - const userInitialValues: AddUserFormModel = { + const userInitialValues: formType = { model: { email: '', password: '', name: '', city: noCitySelected, role: noRoleSelected }, } return ( <div> - <AddCompetitionButton color="secondary" variant="contained" onClick={handleClick}> + <AddButton + style={{ backgroundColor: '#4caf50', color: '#fcfcfc' }} + color="default" + variant="contained" + onClick={handleClick} + endIcon={<PersonAddIcon></PersonAddIcon>} + > Ny Användare - </AddCompetitionButton> + </AddButton> <Popover id={id} open={open} @@ -108,10 +106,10 @@ const AddUser: React.FC = (props: any) => { horizontal: 'center', }} > - <AddCompetitionContent> - <Formik initialValues={userInitialValues} validationSchema={userSchema} onSubmit={handleCompetitionSubmit}> + <AddContent> + <Formik initialValues={userInitialValues} validationSchema={userSchema} onSubmit={handleSubmit}> {(formik) => ( - <AddCompetitionForm onSubmit={formik.handleSubmit}> + <AddForm onSubmit={formik.handleSubmit}> <TextField label="Email" name="model.email" @@ -208,10 +206,10 @@ const AddUser: React.FC = (props: any) => { {formik.errors.error} </Alert> )} - </AddCompetitionForm> + </AddForm> )} </Formik> - </AddCompetitionContent> + </AddContent> </Popover> </div> ) diff --git a/client/src/pages/admin/users/EditUser.tsx b/client/src/pages/admin/users/EditUser.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a1be7c875b031080ccc16ca854a77ae7c2534cb0 --- /dev/null +++ b/client/src/pages/admin/users/EditUser.tsx @@ -0,0 +1,300 @@ +import { + Button, + createStyles, + FormControl, + InputLabel, + makeStyles, + MenuItem, + Popover, + TextField, + Theme, +} from '@material-ui/core' +import MoreHorizIcon from '@material-ui/icons/MoreHoriz' +import { Alert, AlertTitle } from '@material-ui/lab' +import axios from 'axios' +import { Formik, FormikHelpers } from 'formik' +import React, { useEffect } from 'react' +import * as Yup from 'yup' +import { getSearchUsers } from '../../../actions/searchUser' +import { useAppDispatch, useAppSelector } from '../../../hooks' +import { City, Role, User } from '../../../interfaces/ApiModels' +import { EditUserModel, FormModel } from '../../../interfaces/FormModels' +import { AddContent, AddForm } from '../styledComp' + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + textField: { + marginBottom: '10px', + }, + editButton: { + marginTop: '20px', + paddingTop: '10px', + paddingBottom: '10px', + }, + deleteButton: { + marginTop: '40px', + }, + }) +) + +type formType = FormModel<EditUserModel> + +const noRoleSelected = 'Admin' +const noCitySelected = 'Linköping' + +const userSchema: Yup.SchemaOf<formType> = Yup.object({ + model: Yup.object() + .shape({ + name: Yup.string(), + email: Yup.string().email().required('Email krävs'), + password: Yup.string() + .min(6, 'Lösenord måste vara minst 6 tecken.') + .matches(/[a-zA-Z]/, 'Lösenord får enbart innehålla a-z, A-Z.'), + role: Yup.string().required('Roll krävs').notOneOf([noCitySelected], 'Välj en roll'), + city: Yup.string().required('Stad krävs').notOneOf([noRoleSelected], 'Välj en stad'), + }) + .required(), + error: Yup.string().optional(), +}) + +type UserIdProps = { + user: User +} + +const EditUser = ({ user }: UserIdProps) => { + const dispatch = useAppDispatch() + const classes = useStyles() + + const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null) + const [selectedRole, setSelectedRole] = React.useState<Role | undefined>() + const roles = useAppSelector((state) => state.roles.roles) + + const [selectedCity, setSelectedCity] = React.useState<City | undefined>() + const cities = useAppSelector((state) => state.cities.cities) + + const startRole = roles.find((x) => x.id == user.role_id) + const startCity = cities.find((x) => x.id == user.city_id) + + const open = Boolean(anchorEl) + const id = open ? 'simple-popover' : undefined + + useEffect(() => { + setSelectedCity(startCity) + setSelectedRole(startRole) + }, []) + + const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { + setAnchorEl(event.currentTarget) + } + const handleClose = () => { + setAnchorEl(null) + } + + const handleDeleteUsers = async () => { + if (confirm('Are u sure?')) { + await axios + .delete(`/auth/delete/${user.id}`) + .then(() => { + setAnchorEl(null) + dispatch(getSearchUsers()) + }) + .catch(({ response }) => { + console.warn(response.data) + }) + } + } + + const handleSubmit = async (values: formType, actions: FormikHelpers<formType>) => { + const params = { + email: values.model.email, + name: values.model.name, + city_id: selectedCity?.id as number, + role_id: selectedRole?.id as number, + } + const req: any = {} + if (params.email !== user.email) { + req['email'] = params.email + } + if (params.name !== user.name) { + req['name'] = params.name + } + if (params.city_id !== user.city_id) { + req['city_id'] = params.city_id + } + if (params.role_id !== user.role_id) { + req['role_id'] = params.role_id + } + await axios + .put('/users/' + user.id, req) + .then((res) => { + setAnchorEl(null) + dispatch(getSearchUsers()) + }) + .catch(({ response }) => { + console.warn(response.data) + if (response.data && response.data.message) + actions.setFieldError('error', response.data && response.data.message) + else actions.setFieldError('error', 'Something went wrong, please try again') + }) + .finally(() => { + actions.setSubmitting(false) + }) + } + + const userInitialValues: formType = { + model: { + email: user.email as string, + name: user.name as string, + city: startCity?.name as string, + role: startRole?.name as string, + }, + } + return ( + <div> + <Button onClick={handleClick}> + <MoreHorizIcon /> + </Button> + <Popover + id={id} + open={open} + anchorEl={anchorEl} + onClose={handleClose} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + > + <AddContent> + <Formik initialValues={userInitialValues} validationSchema={userSchema} onSubmit={handleSubmit}> + {(formik) => ( + <AddForm onSubmit={formik.handleSubmit}> + <TextField + className={classes.textField} + label="Email" + name="model.email" + helperText={formik.touched.model?.email ? formik.errors.model?.email : ''} + error={Boolean(formik.touched.model?.email && formik.errors.model?.email)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + defaultValue={user.email} + margin="normal" + /> + <TextField + className={classes.textField} + label="Namn" + name="model.name" + helperText={formik.touched.model?.name ? formik.errors.model?.name : ''} + error={Boolean(formik.touched.model?.name && formik.errors.model?.name)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + defaultValue={user.name} + margin="normal" + /> + <FormControl> + <InputLabel shrink id="demo-customized-select-native"> + Region + </InputLabel> + <TextField + className={classes.textField} + select + name="model.city" + id="standard-select-currency" + value={selectedCity ? selectedCity.name : noCitySelected} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + error={Boolean(formik.errors.model?.city && formik.touched.model?.city)} + helperText={formik.touched.model?.city && formik.errors.model?.city} + margin="normal" + > + {cities && + cities.map((city) => ( + <MenuItem key={city.name} value={city.name} onClick={() => setSelectedCity(city)}> + {city.name} + </MenuItem> + ))} + </TextField> + </FormControl> + + <FormControl> + <InputLabel shrink id="demo-customized-select-native"> + Roll + </InputLabel> + <TextField + className={classes.textField} + select + name="model.role" + id="standard-select-currency" + value={selectedRole ? selectedRole.name : noRoleSelected} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + error={Boolean(formik.errors.model?.role && formik.touched.model?.role)} + helperText={formik.touched.model?.role && formik.errors.model?.role} + margin="normal" + > + {roles && + roles.map((role) => ( + <MenuItem key={role.name} value={role.name} onClick={() => setSelectedRole(role)}> + {role.name} + </MenuItem> + ))} + </TextField> + </FormControl> + + <FormControl> + <InputLabel shrink id="demo-customized-select-native"> + Password + </InputLabel> + <TextField + className={classes.textField} + label="Lösenord" + name="model.password" + helperText={formik.touched.model?.password ? formik.errors.model?.password : ''} + error={Boolean(formik.touched.model?.password && formik.errors.model?.password)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + defaultValue="" + margin="normal" + type="password" + /> + </FormControl> + + <Button + className={classes.editButton} + type="submit" + fullWidth + variant="contained" + color="primary" + disabled={!formik.isValid || !formik.values.model?.role || !formik.values.model?.city} + > + Ändra + </Button> + <Button + onClick={handleDeleteUsers} + className={classes.deleteButton} + fullWidth + variant="contained" + color="secondary" + > + Ta bort + </Button> + + {formik.errors.error && ( + <Alert severity="error"> + <AlertTitle>Error</AlertTitle> + {formik.errors.error} + </Alert> + )} + </AddForm> + )} + </Formik> + </AddContent> + </Popover> + </div> + ) +} + +export default EditUser diff --git a/client/src/pages/admin/components/UserManager.tsx b/client/src/pages/admin/users/UserManager.tsx similarity index 82% rename from client/src/pages/admin/components/UserManager.tsx rename to client/src/pages/admin/users/UserManager.tsx index 7bea8713a8bda1c54ac7259cdd0915fc73bc86d9..11a34128b9093784cb79cb7e19bada1a457e6e71 100644 --- a/client/src/pages/admin/components/UserManager.tsx +++ b/client/src/pages/admin/users/UserManager.tsx @@ -1,4 +1,4 @@ -import { Button, Menu, TablePagination, TextField, Typography } from '@material-ui/core' +import { TablePagination, TextField, Typography } from '@material-ui/core' import FormControl from '@material-ui/core/FormControl' import InputLabel from '@material-ui/core/InputLabel' import MenuItem from '@material-ui/core/MenuItem' @@ -11,16 +11,14 @@ import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' import TableHead from '@material-ui/core/TableHead' import TableRow from '@material-ui/core/TableRow' -import MoreHorizIcon from '@material-ui/icons/MoreHoriz' -import axios from 'axios' import React, { useEffect } from 'react' -import { getCities } from '../../../actions/cities' -import { getRoles } from '../../../actions/roles' import { getSearchUsers, setFilterParams } from '../../../actions/searchUser' import { useAppDispatch, useAppSelector } from '../../../hooks' -import { UserFilterParams } from '../../../interfaces/UserData' +import { User } from '../../../interfaces/ApiModels' +import { UserFilterParams } from '../../../interfaces/FilterParams' +import { FilterContainer, TopBar } from '../styledComp' import AddUser from './AddUser' -import { FilterContainer, RemoveCompetition, TopBar } from './styled' +import EditUser from './EditUser' const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -34,8 +32,10 @@ const useStyles = makeStyles((theme: Theme) => ) const UserManager: React.FC = (props: any) => { + const [editAnchorEl, setEditAnchorEl] = React.useState<null | HTMLElement>(null) const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) - const [activeId, setActiveId] = React.useState<number | undefined>(undefined) + + const [selectedUser, setSelectedUser] = React.useState<User | undefined>(undefined) const [timerHandle, setTimerHandle] = React.useState<number | undefined>(undefined) const users = useAppSelector((state) => state.searchUsers.users) const filterParams = useAppSelector((state) => state.searchUsers.filterParams) @@ -45,22 +45,36 @@ const UserManager: React.FC = (props: any) => { const classes = useStyles() const noFilterText = 'Alla' const dispatch = useAppDispatch() - const handleClick = (event: React.MouseEvent<HTMLButtonElement>, id: number) => { + + const open = Boolean(anchorEl) + const id = open ? 'simple-popover' : undefined + + const handleClick = (event: React.MouseEvent<HTMLButtonElement>, user: User) => { setAnchorEl(event.currentTarget) - setActiveId(id) + setSelectedUser(user) } const handleClose = () => { setAnchorEl(null) - setActiveId(undefined) + setSelectedUser(undefined) + console.log('close') + } + + const handleEditClose = () => { + setEditAnchorEl(null) + console.log('edit close') } useEffect(() => { - dispatch(getCities()) - dispatch(getRoles()) dispatch(getSearchUsers()) }, []) + useEffect(() => { + console.log('asd') + setEditAnchorEl(null) + setAnchorEl(null) + }, [users]) + const onSearchChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { if (timerHandle) { clearTimeout(timerHandle) @@ -71,25 +85,16 @@ const UserManager: React.FC = (props: any) => { dispatch(setFilterParams({ ...filterParams, email: event.target.value })) } - const handleDeleteUsers = async () => { - if (activeId) { - await axios - .delete(`/auth/delete/${activeId}`) - .then(() => { - setAnchorEl(null) - dispatch(getSearchUsers()) - }) - .catch(({ response }) => { - console.warn(response.data) - }) - } - } - const handleFilterChange = (newParams: UserFilterParams) => { dispatch(setFilterParams(newParams)) dispatch(getSearchUsers()) } + const handleStateClick = () => { + setEditAnchorEl(anchorEl) + setAnchorEl(null) + } + return ( <div> <TopBar> @@ -171,9 +176,7 @@ const UserManager: React.FC = (props: any) => { <TableCell>{cities.find((city) => city.id === row.city_id)?.name || ''}</TableCell> <TableCell>{roles.find((role) => role.id === row.role_id)?.name || ''}</TableCell> <TableCell align="right"> - <Button onClick={(event) => handleClick(event, row.id)}> - <MoreHorizIcon /> - </Button> + <EditUser user={row}></EditUser> </TableCell> </TableRow> ))} @@ -189,11 +192,6 @@ const UserManager: React.FC = (props: any) => { page={filterParams.page} onChangePage={(event, newPage) => handleFilterChange({ ...filterParams, page: newPage })} /> - <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> - <MenuItem>Redigera</MenuItem> - <MenuItem>Byt lösenord</MenuItem> - <RemoveCompetition onClick={handleDeleteUsers}>Ta bort</RemoveCompetition> - </Menu> </div> ) } diff --git a/client/src/pages/login/components/AdminLogin.tsx b/client/src/pages/login/components/AdminLogin.tsx index cdff4d3fd14a422f22461e1cccd3b1acf26f1e84..39892ba1f4c975e1c53e5583aa50927d029c0644 100644 --- a/client/src/pages/login/components/AdminLogin.tsx +++ b/client/src/pages/login/components/AdminLogin.tsx @@ -6,7 +6,7 @@ import { useHistory } from 'react-router-dom' import * as Yup from 'yup' import { loginUser } from '../../../actions/user' import { useAppDispatch, useAppSelector } from '../../../hooks' -import { AccountLoginModel } from '../../../interfaces/models' +import { AccountLoginModel } from '../../../interfaces/FormModels' import { CenteredCircularProgress, LoginForm } from './styled' interface AccountLoginFormModel { diff --git a/client/src/pages/login/components/CompetitionLogin.tsx b/client/src/pages/login/components/CompetitionLogin.tsx index 9ebf658f80d4281739f45105cb27349a04b25fc3..75f73c5be187569fcc5adec2baf591a7cbdd62d3 100644 --- a/client/src/pages/login/components/CompetitionLogin.tsx +++ b/client/src/pages/login/components/CompetitionLogin.tsx @@ -4,7 +4,7 @@ import axios from 'axios' import { Formik, FormikHelpers } from 'formik' import React from 'react' import * as Yup from 'yup' -import { CompetitionLoginModel } from '../../../interfaces/models' +import { CompetitionLoginModel } from '../../../interfaces/FormModels' import { LoginForm } from './styled' interface CompetitionLoginFormModel { diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index 5a0dbc99f7e98559365e5a1adbea11bd2e13bf03..a5209a16c623cbfa42982eb5782cc421c7e3e639 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -7,8 +7,10 @@ import ListItemText from '@material-ui/core/ListItemText' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import React from 'react' import { useParams } from 'react-router-dom' +import { Content } from '../views/styled' import SettingsPanel from './components/SettingsPanel' -import { SlideListItem, ToolBarContainer, ViewButton, ViewButtonGroup } from './styled' +import SlideEditor from './components/SlideEditor' +import { PresentationEditorContainer, SlideListItem, ToolBarContainer, ViewButton, ViewButtonGroup } from './styled' function createSlide(name: string) { return { name } @@ -28,9 +30,6 @@ const rightDrawerWidth = 390 const useStyles = makeStyles((theme: Theme) => createStyles({ - root: { - display: 'flex', - }, appBar: { width: `calc(100% - ${rightDrawerWidth}px)`, marginLeft: leftDrawerWidth, @@ -71,7 +70,7 @@ const PresentationEditorPage: React.FC = () => { const classes = useStyles() const params: CompetitionParams = useParams() return ( - <div className={classes.root}> + <PresentationEditorContainer> <CssBaseline /> <AppBar position="fixed" className={classes.appBar}> <ToolBarContainer> @@ -120,7 +119,11 @@ const PresentationEditorPage: React.FC = () => { > <SettingsPanel></SettingsPanel> </Drawer> - </div> + + <Content leftDrawerWidth={leftDrawerWidth} rightDrawerWidth={rightDrawerWidth}> + <SlideEditor /> + </Content> + </PresentationEditorContainer> ) } diff --git a/client/src/pages/presentationEditor/components/CheckboxComponent.tsx b/client/src/pages/presentationEditor/components/CheckboxComponent.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cb264712b0b4b57ef7938278471bc812085370cf --- /dev/null +++ b/client/src/pages/presentationEditor/components/CheckboxComponent.tsx @@ -0,0 +1,31 @@ +import { Checkbox } from '@material-ui/core' +import React, { useState } from 'react' +import { Rnd } from 'react-rnd' +import { Component } from '../../../interfaces/ApiModels' +import { Position } from '../../../interfaces/Components' + +type CheckboxComponentProps = { + component: Component +} + +const CheckboxComponent = ({ component }: CheckboxComponentProps) => { + const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y }) + return ( + <Rnd + bounds="parent" + onDragStop={(e, d) => { + setCurrentPos({ x: d.x, y: d.y }) + }} + position={{ x: currentPos.x, y: currentPos.y }} + > + <Checkbox + disableRipple + style={{ + transform: 'scale(3)', + }} + /> + </Rnd> + ) +} + +export default CheckboxComponent diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx index a9a1d7677eca1d3a9fedf55ba0c43a35e19f5979..7446c6d1ee6acec3c05f2305f46f2356739877b4 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -48,9 +48,9 @@ const CompetitionSettings: React.FC = () => { return ( <div className={classes.textInputContainer}> <form noValidate autoComplete="off"> - <TextField className={classes.textInput} id="outlined-basic" label="Tävlingsnamn" variant="outlined" /> + <TextField className={classes.textInput} label="Tävlingsnamn" variant="outlined" /> <Divider /> - <TextField className={classes.textInput} id="outlined-basic" label="Stad" variant="outlined" /> + <TextField className={classes.textInput} label="Stad" variant="outlined" /> </form> <List> diff --git a/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx b/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cffba9e59afa4288f980b9b4d3833494a02ec7d9 --- /dev/null +++ b/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react' +import { Rnd } from 'react-rnd' +import { ImageComponent } from '../../../interfaces/ApiModels' +import { Position, Size } from '../../../interfaces/Components' + +type ImageComponentProps = { + component: ImageComponent +} + +const ImageComponentDisplay = ({ component }: ImageComponentProps) => { + const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y }) + const [currentSize, setCurrentSize] = useState<Size>({ w: component.w, h: component.h }) + return ( + <Rnd + minWidth={50} + minHeight={50} + bounds="parent" + onDragStop={(e, d) => { + setCurrentPos({ x: d.x, y: d.y }) + }} + size={{ width: currentSize.w, height: currentSize.h }} + position={{ x: currentPos.x, y: currentPos.y }} + onResize={(e, direction, ref, delta, position) => { + setCurrentSize({ + w: ref.offsetWidth, + h: ref.offsetHeight, + }) + setCurrentPos(position) + }} + onResizeStop={() => { + console.log('Skicka data till server') + }} + > + <img + src="https://365psd.com/images/previews/c61/cartoon-cow-52394.png" + height={currentSize.h} + width={currentSize.w} + draggable={false} + /> + </Rnd> + ) +} + +export default ImageComponentDisplay diff --git a/client/src/pages/presentationEditor/components/SlideEditor.tsx b/client/src/pages/presentationEditor/components/SlideEditor.tsx new file mode 100644 index 0000000000000000000000000000000000000000..22a73ef3164f0f8609f847563ceb7b014a59c181 --- /dev/null +++ b/client/src/pages/presentationEditor/components/SlideEditor.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { ComponentTypes } from '../../../enum/ComponentTypes' +import CheckboxComponent from './CheckboxComponent' +import ImageComponentDisplay from './ImageComponentDisplay' +import { SlideEditorContainer } from './styled' +import TextComponentDisplay from './TextComponentDisplay' + +const SlideEditor: React.FC = () => { + // const components = useAppSelector(state => state.editor.slide.components) // get the current RichSlide + const components: any[] = [ + { id: 0, x: 15, y: 150, w: 200, h: 300, type: ComponentTypes.Checkbox }, + { id: 1, x: 15, y: 250, w: 200, h: 300, type: ComponentTypes.Checkbox }, + { id: 2, x: 15, y: 350, w: 200, h: 300, type: ComponentTypes.Checkbox }, + { id: 3, x: 300, y: 500, w: 100, h: 300, type: ComponentTypes.Text, text: 'text component', font: 'arial' }, + { id: 4, x: 250, y: 100, w: 200, h: 300, type: ComponentTypes.Image }, + { id: 5, x: 350, y: 100, w: 200, h: 300, type: ComponentTypes.Image }, + ] + return ( + <SlideEditorContainer> + {components.map((component) => { + switch (component.type) { + case ComponentTypes.Checkbox: + return <CheckboxComponent key={component.id} component={component} /> + break + case ComponentTypes.Text: + return <TextComponentDisplay key={component.id} component={component} /> + break + case ComponentTypes.Image: + return <ImageComponentDisplay key={component.id} component={component} /> + break + default: + break + } + })} + </SlideEditorContainer> + ) +} + +export default SlideEditor diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index 936f3d8b5f4285fae1f8be488e608595887515ee..4b0940214a10f2e223b03a418e0dd6209747fff0 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -137,7 +137,7 @@ const SlideSettings: React.FC = () => { {answers.map((answer) => ( <div key={answer.id}> <ListItem divider> - <TextField className={classes.textInput} id="outlined-basic" label={answer.name} variant="outlined" /> + <TextField className={classes.textInput} label={answer.name} variant="outlined" /> <Checkbox color="default" /> <CloseIcon className={classes.clickableIcon} onClick={() => handleCloseAnswerClick(answer.id)} /> </ListItem> @@ -155,7 +155,7 @@ const SlideSettings: React.FC = () => { {texts.map((text) => ( <div key={text.id}> <ListItem divider> - <TextField className={classes.textInput} id="outlined-basic" label={text.name} variant="outlined" /> + <TextField className={classes.textInput} label={text.name} variant="outlined" /> <MoreHorizOutlinedIcon className={classes.clickableIcon} /> <CloseIcon className={classes.clickableIcon} onClick={() => handleCloseTextClick(text.id)} /> </ListItem> diff --git a/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx b/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx new file mode 100644 index 0000000000000000000000000000000000000000..30426b9da323f72f53fc2f99e1959d1914be3dc6 --- /dev/null +++ b/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx @@ -0,0 +1,63 @@ +import { Editor } from '@tinymce/tinymce-react' +import React, { useState } from 'react' +import { Rnd } from 'react-rnd' +import { TextComponent } from '../../../interfaces/ApiModels' +import { Position, Size } from '../../../interfaces/Components' + +type ImageComponentProps = { + component: TextComponent +} + +const TextComponentDisplay = ({ component }: ImageComponentProps) => { + const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y }) + const [currentSize, setCurrentSize] = useState<Size>({ w: component.w, h: component.h }) + const handleEditorChange = (e: any) => { + console.log('Content was updated:', e.target.getContent()) + //TODO: axios.post + } + return ( + <Rnd + minWidth={50} + minHeight={50} + bounds="parent" + onDragStop={(e, d) => { + setCurrentPos({ x: d.x, y: d.y }) + }} + size={{ width: currentSize.w, height: currentSize.h }} + position={{ x: currentPos.x, y: currentPos.y }} + onResize={(e, direction, ref, delta, position) => { + setCurrentSize({ + w: ref.offsetWidth, + h: ref.offsetHeight, + }) + setCurrentPos(position) + }} + onResizeStop={() => { + console.log('skickar till server') + }} + > + <div style={{ height: '100%', width: '100%' }}> + <Editor + initialValue={component.text} + init={{ + body_class: 'mceBlackBody', + height: '100%', + menubar: false, + plugins: [ + 'advlist autolink lists link image charmap print preview anchor', + 'searchreplace visualblocks code fullscreen', + 'insertdatetime media table paste code help wordcount', + ], + toolbar: + 'undo redo | formatselect | fontselect | bold italic backcolor | \ + alignleft aligncenter alignright alignjustify | \ + bullist numlist outdent indent | removeformat | help', + }} + onChange={handleEditorChange} + /> + </div> + </Rnd> + ) +} + +export default TextComponentDisplay diff --git a/client/src/pages/presentationEditor/components/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx index 81459a54e0c71d8d1f60485d8f932a3a04bbaa78..62239c5b9e53b2925aea7cb4d85784cc862f77d8 100644 --- a/client/src/pages/presentationEditor/components/styled.tsx +++ b/client/src/pages/presentationEditor/components/styled.tsx @@ -5,3 +5,11 @@ export const SettingsTab = styled(Tab)` height: 64px; min-width: 195px; ` + +export const SlideEditorContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +` diff --git a/client/src/pages/presentationEditor/styled.tsx b/client/src/pages/presentationEditor/styled.tsx index 6ba71760ad6d7d1608420082934e03087f903d47..462acf2e64259c13ab74830c7d2484fe2881267e 100644 --- a/client/src/pages/presentationEditor/styled.tsx +++ b/client/src/pages/presentationEditor/styled.tsx @@ -19,3 +19,7 @@ export const SlideListItem = styled(ListItem)` text-align: center; height: 60px; ` + +export const PresentationEditorContainer = styled.div` + height: 100%; +` diff --git a/client/src/pages/views/AudienceViewPage.test.tsx b/client/src/pages/views/AudienceViewPage.test.tsx index 1573f3a41555a76a38f5276688626f663d2bc4bd..d00d4277e8b3a5021088b02a13947fb41f28b9f1 100644 --- a/client/src/pages/views/AudienceViewPage.test.tsx +++ b/client/src/pages/views/AudienceViewPage.test.tsx @@ -1,7 +1,13 @@ import { render } from '@testing-library/react' import React from 'react' +import { Provider } from 'react-redux' +import store from '../../store' import AudienceViewPage from './AudienceViewPage' it('renders audience view page', () => { - render(<AudienceViewPage />) + render( + <Provider store={store}> + <AudienceViewPage /> + </Provider> + ) }) diff --git a/client/src/pages/views/JudgeViewPage.test.tsx b/client/src/pages/views/JudgeViewPage.test.tsx index 5ff1cc5d002eba5db9e9f13256ec4b32643aff4f..537dae4c570f3b7ed50dadaa9f60bae1be8823a2 100644 --- a/client/src/pages/views/JudgeViewPage.test.tsx +++ b/client/src/pages/views/JudgeViewPage.test.tsx @@ -1,7 +1,43 @@ import { render } from '@testing-library/react' +import mockedAxios from 'axios' import React from 'react' +import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' +import store from '../../store' import JudgeViewPage from './JudgeViewPage' it('renders judge view page', () => { - render(<JudgeViewPage />) + const compRes: any = { + data: { + slides: [{ id: 0, title: '' }], + }, + } + const teamsRes: any = { + data: { + items: [ + { + id: 1, + name: 'team1', + }, + { + id: 2, + name: 'team2', + }, + ], + count: 2, + total_count: 3, + }, + } + + ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => { + if (path.endsWith('/teams')) return Promise.resolve(teamsRes) + else return Promise.resolve(compRes) + }) + render( + <BrowserRouter> + <Provider store={store}> + <JudgeViewPage /> + </Provider> + </BrowserRouter> + ) }) diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index 457ebe088c03a470f44fb693b7efbdea7f2d24f5..293a1f08ca5a7bff56b980844233bcb879edd215 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -1,7 +1,109 @@ -import React from 'react' +import { Divider, List, ListItemText } from '@material-ui/core' +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' +import React, { useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' +import { getPresentationCompetition, getPresentationTeams, setCurrentSlide } from '../../actions/presentation' +import { useAppDispatch, useAppSelector } from '../../hooks' +import { ViewParams } from '../../interfaces/ViewParams' +import { SlideListItem } from '../presentationEditor/styled' +import JudgeScoreDisplay from './components/JudgeScoreDisplay' +import SlideDisplay from './components/SlideDisplay' +import { + Content, + JudgeAnswersLabel, + JudgeAppBar, + JudgeQuestionsLabel, + JudgeToolbar, + LeftDrawer, + RightDrawer, +} from './styled' + +const leftDrawerWidth = 150 +const rightDrawerWidth = 390 + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + leftDrawerPaper: { + width: leftDrawerWidth, + }, + rightDrawerPaper: { + width: rightDrawerWidth, + }, + toolbar: theme.mixins.toolbar, + }) +) const JudgeViewPage: React.FC = () => { - return <div>Judge</div> + const classes = useStyles() + const { id, code }: ViewParams = useParams() + const dispatch = useAppDispatch() + const [activeSlideIndex, setActiveSlideIndex] = useState<number>(0) + useEffect(() => { + dispatch(getPresentationCompetition(id)) + dispatch(getPresentationTeams(id)) + }, []) + const teams = useAppSelector((state) => state.presentation.teams) + const slides = useAppSelector((state) => state.presentation.competition.slides) + const handleSelectSlide = (index: number) => { + setActiveSlideIndex(index) + dispatch(setCurrentSlide(slides[index])) + } + return ( + <div> + <JudgeAppBar position="fixed"> + <JudgeToolbar> + <JudgeQuestionsLabel variant="h5">Frågor</JudgeQuestionsLabel> + <JudgeAnswersLabel variant="h5">Svar</JudgeAnswersLabel> + </JudgeToolbar> + </JudgeAppBar> + <LeftDrawer + width={leftDrawerWidth} + variant="permanent" + classes={{ + paper: classes.leftDrawerPaper, + }} + anchor="left" + > + <div className={classes.toolbar} /> + <List> + {slides.map((slide, index) => ( + <SlideListItem + selected={index === activeSlideIndex} + onClick={() => handleSelectSlide(index)} + divider + button + key={slide.id} + > + <ListItemText primary={slide.title} /> + </SlideListItem> + ))} + </List> + </LeftDrawer> + <RightDrawer + width={rightDrawerWidth} + variant="permanent" + classes={{ + paper: classes.rightDrawerPaper, + }} + anchor="right" + > + <div className={classes.toolbar} /> + <List> + {teams.map((answer, index) => ( + <div key={answer.name}> + <JudgeScoreDisplay teamIndex={index} /> + <Divider /> + </div> + ))} + </List> + </RightDrawer> + aaa + <Content leftDrawerWidth={leftDrawerWidth} rightDrawerWidth={rightDrawerWidth}> + <div className={classes.toolbar} /> + <SlideDisplay /> + </Content> + </div> + ) } export default JudgeViewPage diff --git a/client/src/pages/views/ParticipantViewPage.test.tsx b/client/src/pages/views/ParticipantViewPage.test.tsx index f7154a9deb127480b2d92db08da12fc352656af3..85360e4ffe754f5887e9b654c45d0c921a67a3a0 100644 --- a/client/src/pages/views/ParticipantViewPage.test.tsx +++ b/client/src/pages/views/ParticipantViewPage.test.tsx @@ -1,7 +1,13 @@ import { render } from '@testing-library/react' import React from 'react' +import { Provider } from 'react-redux' +import store from '../../store' import ParticipantViewPage from './ParticipantViewPage' it('renders participant view page', () => { - render(<ParticipantViewPage />) + render( + <Provider store={store}> + <ParticipantViewPage /> + </Provider> + ) }) diff --git a/client/src/pages/views/ParticipantViewPage.tsx b/client/src/pages/views/ParticipantViewPage.tsx index a5ff1f4f53c3dac87a06fb78cb325ea8a93e2a3d..55c28af06cff72a24862acaa03e8066d8a8f4a0c 100644 --- a/client/src/pages/views/ParticipantViewPage.tsx +++ b/client/src/pages/views/ParticipantViewPage.tsx @@ -1,7 +1,8 @@ import React from 'react' +import SlideDisplay from './components/SlideDisplay' const ParticipantViewPage: React.FC = () => { - return <div>Deltagare</div> + return <SlideDisplay /> } export default ParticipantViewPage diff --git a/client/src/pages/views/PresenterViewPage.test.tsx b/client/src/pages/views/PresenterViewPage.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fd7b0a9692e08354330b3e4db98c046649c0d10a --- /dev/null +++ b/client/src/pages/views/PresenterViewPage.test.tsx @@ -0,0 +1,43 @@ +import { render } from '@testing-library/react' +import mockedAxios from 'axios' +import React from 'react' +import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' +import store from '../../store' +import PresenterViewPage from './PresenterViewPage' + +it('renders presenter view page', () => { + const compRes: any = { + data: { + slides: [{ id: 0, title: '' }], + }, + } + const teamsRes: any = { + data: { + items: [ + { + id: 1, + name: 'team1', + }, + { + id: 2, + name: 'team2', + }, + ], + count: 2, + total_count: 3, + }, + } + + ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => { + if (path.endsWith('/teams')) return Promise.resolve(teamsRes) + else return Promise.resolve(compRes) + }) + render( + <BrowserRouter> + <Provider store={store}> + <PresenterViewPage /> + </Provider> + </BrowserRouter> + ) +}) diff --git a/client/src/pages/views/PresenterViewPage.tsx b/client/src/pages/views/PresenterViewPage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..131bde22f5b23e70e9ac071563395f353b7b9e1f --- /dev/null +++ b/client/src/pages/views/PresenterViewPage.tsx @@ -0,0 +1,74 @@ +import { List, ListItem, Popover } from '@material-ui/core' +import ChevronRightIcon from '@material-ui/icons/ChevronRight' +import React, { useEffect } from 'react' +import { useHistory, useParams } from 'react-router-dom' +import { + getPresentationCompetition, + getPresentationTeams, + setCurrentSlideNext, + setCurrentSlidePrevious, +} from '../../actions/presentation' +import { useAppDispatch, useAppSelector } from '../../hooks' +import { ViewParams } from '../../interfaces/ViewParams' +import SlideDisplay from './components/SlideDisplay' +import { PresenterButton, PresenterContainer, PresenterFooter, PresenterHeader } from './styled' + +const PresenterViewPage: React.FC = () => { + const teams = useAppSelector((state) => state.presentation.teams) + const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null) + const { id, code }: ViewParams = useParams() + const history = useHistory() + const dispatch = useAppDispatch() + useEffect(() => { + dispatch(getPresentationCompetition(id)) + dispatch(getPresentationTeams(id)) + }, []) + const handleOpenPopover = (event: React.MouseEvent<HTMLButtonElement>) => { + setAnchorEl(event.currentTarget) + } + const handleClose = () => { + setAnchorEl(null) + } + return ( + <PresenterContainer> + <PresenterHeader> + <PresenterButton onClick={handleOpenPopover} color="primary" variant="contained"> + Visa ställning + </PresenterButton> + <PresenterButton onClick={() => history.push('/admin')} variant="contained" color="secondary"> + Avsluta tävling + </PresenterButton> + </PresenterHeader> + <SlideDisplay /> + <PresenterFooter> + <PresenterButton onClick={() => dispatch(setCurrentSlidePrevious())} variant="contained"> + <ChevronRightIcon fontSize="large" /> + </PresenterButton> + <PresenterButton onClick={() => dispatch(setCurrentSlideNext())} variant="contained"> + <ChevronRightIcon fontSize="large" /> + </PresenterButton> + </PresenterFooter> + <Popover + open={Boolean(anchorEl)} + anchorEl={anchorEl} + onClose={handleClose} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + > + <List> + {teams.map((team) => ( + <ListItem key={team.id}>{team.name} score: 20</ListItem> + ))} + </List> + </Popover> + </PresenterContainer> + ) +} + +export default PresenterViewPage diff --git a/client/src/pages/views/components/JudgeScoreDisplay.tsx b/client/src/pages/views/components/JudgeScoreDisplay.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ae4d8ab3e7e95a44357565093711d9f4722be0a3 --- /dev/null +++ b/client/src/pages/views/components/JudgeScoreDisplay.tsx @@ -0,0 +1,39 @@ +import { Box, Typography } from '@material-ui/core' +import React from 'react' +import { useAppSelector } from '../../../hooks' +import { AnswerContainer, ScoreDisplayContainer, ScoreDisplayHeader, ScoreInput } from './styled' + +type ScoreDisplayProps = { + teamIndex: number +} +const questionMaxScore = 5 + +const JudgeScoreDisplay = ({ teamIndex }: ScoreDisplayProps) => { + const currentTeam = useAppSelector((state) => state.presentation.teams[teamIndex]) + return ( + <ScoreDisplayContainer> + <ScoreDisplayHeader> + <Typography variant="h5"> + <Box fontWeight="fontWeightBold">{currentTeam.name}</Box> + </Typography> + + <ScoreInput + label="Poäng" + defaultValue={0} + inputProps={{ style: { fontSize: 20 } }} + InputProps={{ disableUnderline: true, inputProps: { min: 0, max: questionMaxScore } }} + type="number" + ></ScoreInput> + </ScoreDisplayHeader> + <Typography variant="h6">Alla poäng: 2 0 0 0 0 0 0 0 0</Typography> + <Typography variant="h6">Total poäng: 9</Typography> + <AnswerContainer> + <Typography variant="body1"> + Svar: blablablablablablablablablabla blablablablabla blablablablabla blablablablablablablablablabla{' '} + </Typography> + </AnswerContainer> + </ScoreDisplayContainer> + ) +} + +export default JudgeScoreDisplay diff --git a/client/src/pages/views/components/SlideDisplay.test.tsx b/client/src/pages/views/components/SlideDisplay.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1a661d3340d503c71e149393db3cb00f1e2406c4 --- /dev/null +++ b/client/src/pages/views/components/SlideDisplay.test.tsx @@ -0,0 +1,13 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import store from '../../../store' +import SlideDisplay from './SlideDisplay' + +it('renders slide display', () => { + render( + <Provider store={store}> + <SlideDisplay /> + </Provider> + ) +}) diff --git a/client/src/pages/views/components/SlideDisplay.tsx b/client/src/pages/views/components/SlideDisplay.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1c10f9cf03ac9c56d555afaa22dabd2dcaf23752 --- /dev/null +++ b/client/src/pages/views/components/SlideDisplay.tsx @@ -0,0 +1,15 @@ +import { Typography } from '@material-ui/core' +import React from 'react' +import { useAppSelector } from '../../../hooks' +import { SlideContainer } from './styled' + +const SlideDisplay: React.FC = () => { + const currentSlide = useAppSelector((state) => state.presentation.slide) + return ( + <SlideContainer> + <Typography variant="h3">{currentSlide.title}</Typography> + </SlideContainer> + ) +} + +export default SlideDisplay diff --git a/client/src/pages/views/components/styled.tsx b/client/src/pages/views/components/styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0034c39beceb8bcb4371589758959c7cae1dd1ab --- /dev/null +++ b/client/src/pages/views/components/styled.tsx @@ -0,0 +1,27 @@ +import { TextField } from '@material-ui/core' +import styled from 'styled-components' + +export const SlideContainer = styled.div` + display: flex; + justify-content: center; +` + +export const ScoreDisplayContainer = styled.div` + padding-top: 5px; + padding-right: 10px; + padding-left: 10px; +` + +export const ScoreDisplayHeader = styled.div` + display: flex; + justify-content: space-between; +` + +export const ScoreInput = styled(TextField)` + width: 40px; +` + +export const AnswerContainer = styled.div` + display: flex; + flex-wrap: wrap; +` diff --git a/client/src/pages/views/styled.tsx b/client/src/pages/views/styled.tsx index 0dfbf495003c6e71fece6dae17bba586c75f1e34..d4a9cf9eaf0a7a44c3f31b31a21e3c30b7222812 100644 --- a/client/src/pages/views/styled.tsx +++ b/client/src/pages/views/styled.tsx @@ -1,5 +1,23 @@ +import { AppBar, Button, Drawer, Toolbar, Typography } from '@material-ui/core' import styled from 'styled-components' +export const JudgeAppBar = styled(AppBar)` + z-index: 9000; +` + +export const JudgeToolbar = styled(Toolbar)` + display: flex; + justify-content: space-between; +` + +export const JudgeQuestionsLabel = styled(Typography)` + margin-left: 15px; +` + +export const JudgeAnswersLabel = styled(Typography)` + margin-right: 160px; +` + export const ViewSelectContainer = styled.div` display: flex; justify-content: center; @@ -15,4 +33,59 @@ export const ViewSelectButtonGroup = styled.div` height: 140px; margin-left: auto; margin-right: auto; -` \ No newline at end of file +` + +export const PresenterHeader = styled.div` + display: flex; + justify-content: space-between; + position: fixed; + width: 100%; +` + +export const PresenterFooter = styled.div` + display: flex; + justify-content: space-between; +` + +export const PresenterButton = styled(Button)` + width: 100px; + height: 100px; + padding-top: 16px; + padding-bottom: 16px; +` + +export const PresenterContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; +` + +interface DrawerProps { + width: number +} + +export const LeftDrawer = styled(Drawer)<DrawerProps>` + flex-shrink: 0; + position: 'relative'; + z-index: -5; + width: ${(props) => (props ? props.width : 150)}; +` + +export const RightDrawer = styled(Drawer)<DrawerProps>` + width: ${(props) => (props ? props.width : 150)}; + flex-shrink: 0; + z-index: 1; +` + +interface ContentProps { + leftDrawerWidth: number + rightDrawerWidth: number +} + +export const Content = styled.div<ContentProps>` + margin-left: ${(props) => (props ? props.leftDrawerWidth : 0)}px; + margin-right: ${(props) => (props ? props.rightDrawerWidth : 0)}px; + width: calc(100% - ${(props) => (props ? props.leftDrawerWidth + props.rightDrawerWidth : 0)}px); + height: calc(100% - 64px); +` diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index 3c9f2b6cecf5166e9c21c8366b97345a2e2827ec..94743ff18c546c80e9ce7400e5afdace7e871bf4 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -3,6 +3,7 @@ import { combineReducers } from 'redux' import citiesReducer from './citiesReducer' import competitionsReducer from './competitionsReducer' +import presentationReducer from './presentationReducer' import rolesReducer from './rolesReducer' import searchUserReducer from './searchUserReducer' import uiReducer from './uiReducer' @@ -14,6 +15,7 @@ const allReducers = combineReducers({ UI: uiReducer, competitions: competitionsReducer, cities: citiesReducer, + presentation: presentationReducer, roles: rolesReducer, searchUsers: searchUserReducer, }) diff --git a/client/src/reducers/citiesReducer.ts b/client/src/reducers/citiesReducer.ts index 1c9542ff0c6ec2059a56f1ff5b5ac58c046114c6..7f5555b3eb002559df3952dfbac31d178ffd6271 100644 --- a/client/src/reducers/citiesReducer.ts +++ b/client/src/reducers/citiesReducer.ts @@ -1,6 +1,6 @@ import { AnyAction } from 'redux' import Types from '../actions/types' -import { City } from '../interfaces/City' +import { City } from '../interfaces/ApiModels' interface CityState { cities: City[] diff --git a/client/src/reducers/competitionsReducer.ts b/client/src/reducers/competitionsReducer.ts index b3003d4a366a95665779b4fc1fafbd7af8cd352e..bb788da5439874f8f9963dfbb756a222012697e6 100644 --- a/client/src/reducers/competitionsReducer.ts +++ b/client/src/reducers/competitionsReducer.ts @@ -1,7 +1,7 @@ import { AnyAction } from 'redux' import Types from '../actions/types' -import { Competition } from '../interfaces/Competition' -import { CompetitionFilterParams } from './../interfaces/CompetitionFilterParams' +import { Competition } from '../interfaces/ApiModels' +import { CompetitionFilterParams } from './../interfaces/FilterParams' interface CompetitionState { competitions: Competition[] diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..d71bcafbca4720c9d82ed41a18a6e55db6d91927 --- /dev/null +++ b/client/src/reducers/presentationReducer.ts @@ -0,0 +1,72 @@ +import { AnyAction } from 'redux' +import Types from '../actions/types' +import { RichCompetition } from './../interfaces/ApiRichModels' +import { Slide } from './../interfaces/Slide' +import { Team } from './../interfaces/Team' + +interface PresentationState { + competition: RichCompetition + slide: Slide + teams: Team[] +} + +const initialState: PresentationState = { + competition: { + name: '', + id: 0, + city: { + id: 0, + name: '', + }, + slides: [], + year: 0, + teams: [], + }, + slide: { + competition_id: 0, + id: 0, + order: 0, + timer: 0, + title: '', + }, + teams: [], +} + +export default function (state = initialState, action: AnyAction) { + switch (action.type) { + case Types.SET_PRESENTATION_COMPETITION: + return { + ...state, + slide: action.payload.slides[0] as Slide, + competition: action.payload as RichCompetition, + } + case Types.SET_PRESENTATION_TEAMS: + return { + ...state, + teams: action.payload as Team[], + } + case Types.SET_PRESENTATION_SLIDE: + return { + ...state, + slide: action.payload as Slide, + } + case Types.SET_PRESENTATION_SLIDE_PREVIOUS: + if (state.slide.order - 1 >= 0) { + return { + ...state, + slide: state.competition.slides[state.slide.order - 1], + } + } + return state + case Types.SET_PRESENTATION_SLIDE_NEXT: + if (state.slide.order + 1 < state.competition.slides.length) { + return { + ...state, + slide: state.competition.slides[state.slide.order + 1], + } + } + return state + default: + return state + } +} diff --git a/client/src/reducers/rolesReducer.ts b/client/src/reducers/rolesReducer.ts index 652d4afcf12c1f58aad0c1b9cf21ab36e2ca96da..5028ae04cb13a4b1bf44536cd42bf3f8935b268c 100644 --- a/client/src/reducers/rolesReducer.ts +++ b/client/src/reducers/rolesReducer.ts @@ -1,6 +1,6 @@ import { AnyAction } from 'redux' import Types from '../actions/types' -import { Role } from '../interfaces/Role' +import { Role } from '../interfaces/ApiModels' interface RoleState { roles: Role[] diff --git a/client/src/reducers/searchUserReducer.ts b/client/src/reducers/searchUserReducer.ts index 9b77d68af26b3fbf2f0b428c8a6a959f6e9c5991..e0c1250683ae273318a4bcd6e4a3f5ae5f6324bd 100644 --- a/client/src/reducers/searchUserReducer.ts +++ b/client/src/reducers/searchUserReducer.ts @@ -1,9 +1,10 @@ import { AnyAction } from 'redux' import Types from '../actions/types' -import { UserData, UserFilterParams } from '../interfaces/UserData' +import { User } from '../interfaces/ApiModels' +import { UserFilterParams } from '../interfaces/FilterParams' interface SearchUserState { - users: UserData[] + users: User[] total: number count: number filterParams: UserFilterParams @@ -21,7 +22,7 @@ export default function (state = initialState, action: AnyAction) { case Types.SET_SEARCH_USERS: return { ...state, - users: action.payload as UserData[], + users: action.payload as User[], } case Types.SET_SEARCH_USERS_FILTER_PARAMS: return { diff --git a/client/src/reducers/userReducer.ts b/client/src/reducers/userReducer.ts index 9a26b1c9f8d62a61733299ccfff48c24d67310d1..6064292aa9ce8d325536d5fea5febb0472ecd4e7 100644 --- a/client/src/reducers/userReducer.ts +++ b/client/src/reducers/userReducer.ts @@ -1,12 +1,11 @@ import { AnyAction } from 'redux' import Types from '../actions/types' -import { City } from '../interfaces/City' -import { Competition } from './../interfaces/Competition' +import { City, Role } from '../interfaces/ApiModels' interface UserInfo { name: string email: string - role: Competition + role: Role city: City id: number } diff --git a/client/src/styled.tsx b/client/src/styled.tsx index 0c17f9cc13be00e9d7d876c726fefc860361d380..58cf0c24750f2db3c5fb98e9aa6a57482d1d717a 100644 --- a/client/src/styled.tsx +++ b/client/src/styled.tsx @@ -1,6 +1,5 @@ import styled from 'styled-components' export const Wrapper = styled.div` - padding: 10px; height: 100%; `