From 6fda4d61d74213c1111649385bad1f4d0ff86162 Mon Sep 17 00:00:00 2001 From: maxime Date: Mon, 8 Jan 2024 22:35:00 +0100 Subject: [PATCH 1/8] format --- front/components/arrows/BendableArrow.tsx | 31 ++++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/front/components/arrows/BendableArrow.tsx b/front/components/arrows/BendableArrow.tsx index 6ecdb44..b8f0f19 100644 --- a/front/components/arrows/BendableArrow.tsx +++ b/front/components/arrows/BendableArrow.tsx @@ -116,23 +116,26 @@ export default function BendableArrow({ const styleWidth = style?.width ?? ArrowStyleDefaults.width - const computeInternalSegments = useCallback((segments: Segment[]) => { - return segments.map((segment, idx) => { - if (idx == 0) { + const computeInternalSegments = useCallback( + (segments: Segment[]) => { + return segments.map((segment, idx) => { + if (idx == 0) { + return { + start: startPos, + controlPoint: segment.controlPoint ?? null, + end: segment.next, + } + } + const start = segments[idx - 1].next return { - start: startPos, + start, controlPoint: segment.controlPoint ?? null, end: segment.next, } - } - const start = segments[idx - 1].next - return { - start, - controlPoint: segment.controlPoint ?? null, - end: segment.next, - } - }) - }, [segments, startPos]) + }) + }, + [segments, startPos], + ) // Cache the segments so that when the user is changing the segments (it moves an ArrowPoint), // it does not unwind to this arrow's component parent until validated. @@ -151,8 +154,6 @@ export default function BendableArrow({ const headRef = useRef(null) const tailRef = useRef(null) - - /** * Computes and return the segments edition points * @param parentBase From 2b49a1393c31b34d641a3c7db3daed67f5cef677 Mon Sep 17 00:00:00 2001 From: Yanis DAHMANE-BOUNOUA Date: Mon, 8 Jan 2024 23:11:44 +0100 Subject: [PATCH 2/8] home-page (#81) Co-authored-by: d_yanis Co-authored-by: DahmaneYanis Co-authored-by: maxime Reviewed-on: https://codefirst.iut.uca.fr/git/IQBall/Application-Web/pulls/81 Co-authored-by: Yanis DAHMANE-BOUNOUA Co-committed-by: Yanis DAHMANE-BOUNOUA --- .gitignore | 1 + config.php | 1 + front/assets/account.svg | 1 + front/assets/court/court.svg | 63 ++++++ front/components/editor/BasketCourt.tsx | 1 + front/style/ball.css | 3 + front/style/colors.css | 13 -- front/style/home/home.css | 43 ++++ front/style/home/personnal_space.css | 40 ++++ front/style/home/side_menu.css | 53 +++++ front/style/template/header.css | 65 ++++++ front/style/theme/dark.css | 9 + front/views/Home.tsx | 267 ++++++++++++++++++++++++ front/views/template/Header.tsx | 36 ++++ public/account.svg | 1 + public/index.php | 4 +- src/App/Controller/UserController.php | 32 ++- src/Core/Gateway/TacticInfoGateway.php | 35 +++- src/Core/Gateway/TeamGateway.php | 9 + src/Core/Model/TacticModel.php | 22 +- src/Core/Model/TeamModel.php | 10 + 21 files changed, 682 insertions(+), 27 deletions(-) create mode 100644 front/assets/account.svg create mode 100644 front/assets/court/court.svg delete mode 100644 front/style/colors.css create mode 100644 front/style/home/home.css create mode 100644 front/style/home/personnal_space.css create mode 100644 front/style/home/side_menu.css create mode 100644 front/style/template/header.css create mode 100644 front/style/theme/dark.css create mode 100644 front/views/Home.tsx create mode 100644 front/views/template/Header.tsx create mode 100644 public/account.svg diff --git a/.gitignore b/.gitignore index a02dfdf..3934c5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vs +.vscode .idea .code .vite diff --git a/config.php b/config.php index 6e510c8..fdf02a4 100644 --- a/config.php +++ b/config.php @@ -11,6 +11,7 @@ const SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH; * Maps the given relative source uri (relative to the `/front` folder) to its actual location depending on imported profile. * @param string $assetURI relative uri path from `/front` folder * @return string valid url that points to the given uri + */ function asset(string $assetURI): string { return _asset($assetURI); diff --git a/front/assets/account.svg b/front/assets/account.svg new file mode 100644 index 0000000..70d7391 --- /dev/null +++ b/front/assets/account.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/assets/court/court.svg b/front/assets/court/court.svg new file mode 100644 index 0000000..e01fd58 --- /dev/null +++ b/front/assets/court/court.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index e57d669..e59bdb7 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -8,6 +8,7 @@ import { useState, } from "react" import CourtPlayer from "./CourtPlayer" + import { Player } from "../../tactic/Player" import { Action, ActionKind } from "../../tactic/Action" import ArrowAction from "../actions/ArrowAction" diff --git a/front/style/ball.css b/front/style/ball.css index c14c196..5169ec7 100644 --- a/front/style/ball.css +++ b/front/style/ball.css @@ -9,3 +9,6 @@ height: 20px; cursor: pointer; } + +.ball-div:focus-within { +} diff --git a/front/style/colors.css b/front/style/colors.css deleted file mode 100644 index db7edfe..0000000 --- a/front/style/colors.css +++ /dev/null @@ -1,13 +0,0 @@ -:root { - --main-color: #ffffff; - --second-color: #ccde54; - - --background-color: #d2cdd3; - - --selected-team-primarycolor: #ffffff; - --selected-team-secondarycolor: #000000; - - --selection-color: #3f7fc4; - - --arrows-color: #676767; -} diff --git a/front/style/home/home.css b/front/style/home/home.css new file mode 100644 index 0000000..455e3af --- /dev/null +++ b/front/style/home/home.css @@ -0,0 +1,43 @@ +@import url(../theme/dark.css); +@import url(personnal_space.css); +@import url(side_menu.css); +@import url(../template/header.css); + +body { + /* background-color: #303030; */ +} +#main { + /* margin-left : 10%; + margin-right: 10%; */ + /* border : solid 1px #303030; */ + display: flex; + flex-direction: column; + font-family: var(--font-content); + height: 100%; +} + +#body { + display: flex; + flex-direction: row; + margin: 0px; + height: 100%; + background-color: var(--second-color); +} + +.data { + border: 1.5px solid var(--main-contrast-color); + background-color: var(--main-color); + border-radius: 0.75cap; + color: var(--main-contrast-color); +} + +.data:hover { + border-color: var(--accent-color); + cursor: pointer; +} + +.set-button { + width: 80%; + margin-left: 5%; + margin-top: 5%; +} diff --git a/front/style/home/personnal_space.css b/front/style/home/personnal_space.css new file mode 100644 index 0000000..173098e --- /dev/null +++ b/front/style/home/personnal_space.css @@ -0,0 +1,40 @@ +#personal-space { + display: flex; + flex-direction: column; +} + +#title-personal-space h2 { + text-align: center; + color: var(--main-contrast-color); + /* font-family: Helvetica; + font-weight: bold; */ +} + +#body-personal-space { + width: 95%; + /* background-color: #ccc2b7; */ + border: 3px var(--main-color) solid; + border-radius: 0.5cap; + align-self: center; +} + +#body-personal-space table { + width: 100%; + border-collapse: separate; + border-spacing: 1em; + table-layout: fixed; + overflow: hidden; +} + +#body-personal-space td { + width: 80px !important; + padding-bottom: 1%; + padding-top: 1%; + height: fit-content; + text-align: center; + overflow: hidden; +} + +tbody p { + text-align: center; +} diff --git a/front/style/home/side_menu.css b/front/style/home/side_menu.css new file mode 100644 index 0000000..3a23947 --- /dev/null +++ b/front/style/home/side_menu.css @@ -0,0 +1,53 @@ +@import url(../theme/dark.css); + +#side-menu { + background-color: var(--third-color); + display: flex; + flex-direction: column; + align-items: center; + overflow: hidden; +} + +#side-menu h2 { + display: inline-block; + margin-right: 5%; +} + +#side-menu-content { + width: 90%; +} +.titre-side-menu { + border-bottom: var(--main-color) solid 3px; + width: 100%; + margin-bottom: 3%; +} + +#side-menu .title { + font-size: 12px; + font-weight: bold; + color: var(--main-contrast-color); + letter-spacing: 1px; + text-transform: uppercase; + background-color: var(--main-color); + padding: 3%; + margin-bottom: 0px; + margin-right: 3%; +} + +.new { + border-radius: 100%; +} + +.button-side-menu { + /* border : black solid 1px; */ + border-radius: 0.5cap; + width: fit-content; + padding: 2%; + margin-top: 3%; + overflow: hidden; +} + +.button-side-menu:hover { + /* background-color: #c9d1e0; */ + cursor: pointer; +} diff --git a/front/style/template/header.css b/front/style/template/header.css new file mode 100644 index 0000000..2ea8d2f --- /dev/null +++ b/front/style/template/header.css @@ -0,0 +1,65 @@ +#header { + text-align: center; + background-color: var(--main-color); + margin: 0px; + /* border : var(--accent-color) 1px solid; */ + display: flex; + flex-direction: row; + font-family: var(--font-title); + /* border-radius: 0.75cap; */ +} + +#img-account { + width: 100%; + cursor: pointer; +} + +#header-right, +#header-left { + width: 10%; + /* border: yellow 2px solid; */ +} + +#header-right { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +#username { + color: var(--main-contrast-color); + margin: 0; +} + +#clickable-header-right:hover #username { + color: var(--accent-color); +} + +#header-center { + width: 80%; +} + +#clickable-header-right { + width: 40%; + border-radius: 1cap; + padding: 2%; +} + +#clickable-header-right:hover { + border: orange 1px solid; +} + +.clickable { + cursor: pointer; +} + +#img-account { + width: 100%; +} + +#iqball { + color: var(--accent-color); + font-weight: bold; + font-size: 45px; +} diff --git a/front/style/theme/dark.css b/front/style/theme/dark.css new file mode 100644 index 0000000..bdd4824 --- /dev/null +++ b/front/style/theme/dark.css @@ -0,0 +1,9 @@ +:root { + --main-color: #191a21; + --second-color: #282a36; + --third-color: #303341; + --accent-color: #ffa238; + --main-contrast-color: #e6edf3; + --font-title: Helvetica; + --font-content: Helvetica; +} diff --git a/front/views/Home.tsx b/front/views/Home.tsx new file mode 100644 index 0000000..44803a3 --- /dev/null +++ b/front/views/Home.tsx @@ -0,0 +1,267 @@ +import "../style/home/home.css" + +// import AccountSvg from "../assets/account.svg?react" +import { CSSProperties, useRef } from "react" +import { Header } from "./template/Header" + +interface Tactic { + id: number + name: string + creation_date: string +} + +interface Team { + id: number + name: string + picture: string + main_color: string + second_color: string +} + +export default function Home({ + lastTactics, + allTactics, + teams, + username, +}: { + lastTactics: Tactic[] + allTactics: Tactic[] + teams: Team[] + username: string +}) { + return ( +
+
+ +
+ ) +} + +function Body({ + lastTactics, + allTactics, + teams, +}: { + lastTactics: Tactic[] + allTactics: Tactic[] + teams: Team[] +}) { + const widthPersonalSpace = 78 + const widthSideMenu = 100 - widthPersonalSpace + return ( +
+ + +
+ ) +} + +function SideMenu({ + width, + lastTactics, + teams, +}: { + width: number + lastTactics: Tactic[] + teams: Team[] +}) { + return ( +
+
+ + +
+
+ ) +} + +function PersonalSpace({ + width, + allTactics, +}: { + width: number + allTactics: Tactic[] +}) { + return ( +
+ + +
+ ) +} + +function TitlePersonalSpace() { + return ( +
+

Espace Personnel

+
+ ) +} + +function TableData({ allTactics }: { allTactics: Tactic[] }) { + const nbRow = Math.floor(allTactics.length / 3) + 1 + let listTactic = Array(nbRow) + for (let i = 0; i < nbRow; i++) { + listTactic[i] = Array(0) + } + let i = 0 + let j = 0 + allTactics.forEach((tactic) => { + listTactic[i].push(tactic) + j++ + if (j === 3) { + i++ + j = 0 + } + }) + + i = 0 + while (i < nbRow) { + listTactic[i] = listTactic[i].map((tactic: Tactic) => ( + { + location.pathname = "/tactic/" + tactic.id + "/edit" + }}> + {truncateString(tactic.name, 25)} + + )) + i++ + } + if (nbRow == 1) { + if (listTactic[0].length < 3) { + for (let i = 0; i <= 3 - listTactic[0].length; i++) { + listTactic[0].push() + } + } + } + + const data = listTactic.map((tactic, rowIndex) => ( + {tactic} + )) + return data +} + +function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) { + let data + if (allTactics.length == 0) { + data =

Aucune tactique créé !

+ } else { + data = + } + + return ( +
+ + {data} +
+
+ ) +} + +function Team({ teams }: { teams: Team[] }) { + const listTeam = teams.map((team, rowIndex) => ( +
  • + {team.name} + +
  • + )) + return ( +
    +
    +

    Mes équipes

    + +
    + +
    + ) +} + +function Tactic({ lastTactics }: { lastTactics: Tactic[] }) { + return ( +
    +
    +

    Mes dernières stratégies

    + +
    + +
    + ) +} + +function SetButtonTactic({ tactics }: { tactics: Tactic[] }) { + const lastTactics = tactics.map((tactic) => ( + + )) + return
    {lastTactics}
    +} + +function SetButtonTeam({ teams }: { teams: Team[] }) { + const listTeam = teams.map((teams) => ) + return
    {listTeam}
    +} + +function ButtonTeam({ team }: { team: Team }) { + const name = truncateString(team.name, 20) + return ( +
    +
    { + location.pathname = "/team/" + team.id + }}> + {name} +
    +
    + ) +} + +function ButtonLastTactic({ tactic }: { tactic: Tactic }) { + const name = truncateString(tactic.name, 20) + return ( +
    { + location.pathname = "/tactic/" + tactic.id + "/edit" + }}> + {name} +
    + ) +} + +function truncateString(name: string, limit: number): string { + if (name.length > limit) { + name = name.substring(0, limit) + "..." + } + return name +} diff --git a/front/views/template/Header.tsx b/front/views/template/Header.tsx new file mode 100644 index 0000000..7129153 --- /dev/null +++ b/front/views/template/Header.tsx @@ -0,0 +1,36 @@ +/** + * + * @param param0 username + * @returns Header + */ +export function Header({ username }: { username: string }) { + return ( + + ) +} diff --git a/public/account.svg b/public/account.svg new file mode 100644 index 0000000..70d7391 --- /dev/null +++ b/public/account.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/index.php b/public/index.php index 8c9a62b..935e2f2 100644 --- a/public/index.php +++ b/public/index.php @@ -1,6 +1,5 @@ tactics = $tactics; + $this->teams = $teams; } /** @@ -23,16 +27,34 @@ class UserController { * @return ViewHttpResponse the home page view */ public function home(SessionHandle $session): ViewHttpResponse { - //TODO use session's account to get the last 5 tactics of the logged-in account - $listTactic = $this->tactics->getLast(5); - return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]); + $limitNbTactics = 5; + $lastTactics = $this->tactics->getLast($limitNbTactics, $session->getAccount()->getId()); + $allTactics = $this->tactics->getAll($session->getAccount()->getId()); + $name = $session->getAccount()->getName(); + + if ($this->teams != null) { + $teams = $this->teams->getAll($session->getAccount()->getId()); + } else { + $teams = []; + } + + return ViewHttpResponse::react("views/Home.tsx", [ + "lastTactics" => $lastTactics, + "allTactics" => $allTactics, + "teams" => $teams, + "username" => $name, + ]); + } + + public function homeTwig(SessionHandle $session): ViewHttpResponse { + return ViewHttpResponse::twig("home.twig", []); } /** * @return ViewHttpResponse account settings page */ public function settings(SessionHandle $session): ViewHttpResponse { - return ViewHttpResponse::twig("account_settings.twig", []); + return ViewHttpResponse::react("views/Settings.tsx", []); } public function disconnect(MutableSessionHandle $session): HttpResponse { diff --git a/src/Core/Gateway/TacticInfoGateway.php b/src/Core/Gateway/TacticInfoGateway.php index 67cffc4..08302c9 100644 --- a/src/Core/Gateway/TacticInfoGateway.php +++ b/src/Core/Gateway/TacticInfoGateway.php @@ -45,13 +45,40 @@ class TacticInfoGateway { * @param integer $nb * @return array> */ - public function getLast(int $nb): ?array { + public function getLast(int $nb, int $ownerId): ?array { $res = $this->con->fetch( - "SELECT * FROM Tactic ORDER BY creation_date DESC LIMIT :nb ", - [":nb" => [$nb, PDO::PARAM_INT]] + "SELECT * + FROM Tactic + WHERE owner = :ownerId + ORDER BY creation_date DESC + LIMIT :nb", + [ + ":ownerId" => [$ownerId, PDO::PARAM_INT],":nb" => [$nb, PDO::PARAM_INT], + ] ); if (count($res) == 0) { - return null; + return []; + } + return $res; + } + + /** + * Get all the tactics of the owner + * + * @return array> + */ + public function getAll(int $ownerId): ?array { + $res = $this->con->fetch( + "SELECT * + FROM Tactic + WHERE owner = :ownerId + ORDER BY name DESC", + [ + ":ownerId" => [$ownerId, PDO::PARAM_INT], + ] + ); + if (count($res) == 0) { + return []; } return $res; } diff --git a/src/Core/Gateway/TeamGateway.php b/src/Core/Gateway/TeamGateway.php index d775eda..bc1da94 100644 --- a/src/Core/Gateway/TeamGateway.php +++ b/src/Core/Gateway/TeamGateway.php @@ -81,5 +81,14 @@ class TeamGateway { )[0]['id'] ?? null; } + /** + * Get all the user's teams + * + * @param integer $user + * @return array> + */ + public function getAll(int $user): array { + return $this->con->fetch("SELECT * FROM Team", []); + } } diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php index 51e5eb8..7057e7f 100644 --- a/src/Core/Model/TacticModel.php +++ b/src/Core/Model/TacticModel.php @@ -3,6 +3,7 @@ namespace IQBall\Core\Model; use IQBall\Core\Data\CourtType; +use IQBall\App\Session\SessionHandle; use IQBall\Core\Data\TacticInfo; use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Validation\ValidationFail; @@ -57,10 +58,27 @@ class TacticModel { * @param integer $nb * @return array> */ - public function getLast(int $nb): ?array { - return $this->tactics->getLast($nb); + + /** + * Return the nb last tactics + * + * @param integer $nb + * @param integer $ownerId + * @return array> + */ + public function getLast(int $nb, int $ownerId): array { + return $this->tactics->getLast($nb, $ownerId); } + /** + * Get all the tactics of the owner + * + * @param integer $ownerId + * @return array> + */ + public function getAll(int $ownerId): ?array { + return $this->tactics->getAll($ownerId); + } /** * Update the name of a tactic * @param int $id the tactic identifier diff --git a/src/Core/Model/TeamModel.php b/src/Core/Model/TeamModel.php index f6af837..a5ba84b 100644 --- a/src/Core/Model/TeamModel.php +++ b/src/Core/Model/TeamModel.php @@ -79,4 +79,14 @@ class TeamModel { return $teamId; } + /** + * Get all user's teams + * + * @param integer $user + * @return array> + */ + public function getAll(int $user): array { + return $this->teams->getAll($user); + } + } From 939a611e451d3c1ebb10cfabb178d79194d9353a Mon Sep 17 00:00:00 2001 From: maxime Date: Mon, 8 Jan 2024 23:46:06 +0100 Subject: [PATCH 3/8] fix franglish and small court --- front/assets/court/half_court.svg | 1 - front/components/editor/SavingState.tsx | 8 ++++---- front/style/editor.css | 5 +++-- front/style/new_tactic_panel.css | 13 +------------ front/views/NewTacticPanel.tsx | 11 +++-------- 5 files changed, 11 insertions(+), 27 deletions(-) diff --git a/front/assets/court/half_court.svg b/front/assets/court/half_court.svg index 7bf82e0..f621f93 100644 --- a/front/assets/court/half_court.svg +++ b/front/assets/court/half_court.svg @@ -1,5 +1,4 @@ - diff --git a/front/components/editor/SavingState.tsx b/front/components/editor/SavingState.tsx index 68c2285..ed699b4 100644 --- a/front/components/editor/SavingState.tsx +++ b/front/components/editor/SavingState.tsx @@ -6,19 +6,19 @@ export interface SaveState { export class SaveStates { static readonly Guest: SaveState = { className: "save-state-guest", - message: "you are not connected, your changes will not be saved.", + message: "vous n'etes pas connectés, les changements seront sauvegardés sur votre navigateur.", } static readonly Ok: SaveState = { className: "save-state-ok", - message: "saved", + message: "sauvegardé", } static readonly Saving: SaveState = { className: "save-state-saving", - message: "saving...", + message: "sauvegarde...", } static readonly Err: SaveState = { className: "save-state-error", - message: "could not save tactic.", + message: "erreur lors de la sauvegarde.", } } diff --git a/front/style/editor.css b/front/style/editor.css index 258476a..d81f38a 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -47,6 +47,8 @@ height: 100%; } + + #allies-rack, #opponent-rack { width: 125px; @@ -95,8 +97,7 @@ #court-image-div { position: relative; background-color: white; - height: 100%; - width: 100%; + height: 80vh; } .court-container { diff --git a/front/style/new_tactic_panel.css b/front/style/new_tactic_panel.css index ff6a07e..b1043d8 100644 --- a/front/style/new_tactic_panel.css +++ b/front/style/new_tactic_panel.css @@ -104,19 +104,8 @@ background-color: var(--second-color); } -.court-kind-button-name, -.court-kind-button-details { +.court-kind-button-name { user-select: none; font-family: var(--text-main-font); } -.court-kind-button-details { - position: absolute; - z-index: -1; - top: 0; - transition: top 1s; -} - -.court-kind-button:hover .court-kind-button-details { - top: -20px; -} diff --git a/front/views/NewTacticPanel.tsx b/front/views/NewTacticPanel.tsx index 97ec0a5..d02f314 100644 --- a/front/views/NewTacticPanel.tsx +++ b/front/views/NewTacticPanel.tsx @@ -9,19 +9,17 @@ export default function NewTacticPanel() { return (
    -

    Select a basket court

    +

    Selectionnez un terrain

    @@ -34,19 +32,16 @@ export default function NewTacticPanel() { function CourtKindButton({ name, image, - details, redirect, }: { name: string image: string - details: string redirect: string }) { return (
    (location.href = BASE + redirect)}> -
    {details}
    Date: Tue, 9 Jan 2024 00:36:33 +0100 Subject: [PATCH 4/8] Team's part (#84) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request adds the members to the team with the possibilitie to add or remove a player or quit the team. It means that this PR implements the account within the team's part of the application. Moreover some bugs has been fixed. The view of the team has been translated to react.js This PR also adds the edit and delete of a team. Co-authored-by: mael.daim Co-authored-by: maxime Reviewed-on: https://codefirst.iut.uca.fr/git/IQBall/Application-Web/pulls/84 Co-authored-by: Maël DAIM Co-committed-by: Maël DAIM --- front/components/editor/BasketCourt.tsx | 9 +- front/components/editor/CourtBall.tsx | 2 +- front/components/editor/CourtPlayer.tsx | 2 +- front/components/editor/PlayerPiece.tsx | 4 +- front/model/Team.ts | 19 ++ front/model/User.ts | 6 + front/{ => model}/tactic/Action.ts | 4 +- .../CourtObjects.ts => model/tactic/Ball.ts} | 0 front/{ => model}/tactic/Player.ts | 9 +- front/{ => model}/tactic/Tactic.ts | 2 +- front/style/team_panel.css | 135 ++++++++++++++ front/tactic/Team.ts | 4 - front/views/Editor.tsx | 26 +-- front/views/Home.tsx | 2 +- front/views/TeamPanel.tsx | 169 ++++++++++++++++++ front/views/editor/CourtAction.tsx | 2 +- public/api/index.php | 1 - public/index.php | 11 +- sql/setup-tables.sql | 3 +- src/Api/Controller/APITacticController.php | 2 +- src/App/Controller/EditorController.php | 4 +- src/App/Controller/TeamController.php | 151 ++++++++++++---- src/App/Controller/UserController.php | 18 +- src/App/Controller/VisualizerController.php | 2 +- src/App/Views/add_member.html.twig | 29 ++- src/App/Views/display_team.html.twig | 56 ++++-- src/App/Views/display_teams.html.twig | 38 +++- src/App/Views/edit_team.html.twig | 81 +++++++++ src/App/Views/home.twig | 4 +- src/App/Views/insert_team.html.twig | 3 +- src/App/Views/list_team_by_name.html.twig | 7 +- src/Core/Data/Account.php | 40 ++--- src/Core/Data/Color.php | 44 ----- src/Core/Data/Member.php | 44 ++--- src/Core/Data/MemberRole.php | 68 ------- src/Core/Data/Team.php | 8 +- src/Core/Data/TeamInfo.php | 20 ++- src/Core/Data/User.php | 72 ++++++++ src/Core/Gateway/AccountGateway.php | 11 +- src/Core/Gateway/MemberGateway.php | 39 +++- src/Core/Gateway/TeamGateway.php | 76 ++++++-- src/Core/Model/AuthModel.php | 11 +- src/Core/Model/TeamModel.php | 89 +++++++-- 43 files changed, 988 insertions(+), 339 deletions(-) create mode 100644 front/model/Team.ts create mode 100644 front/model/User.ts rename front/{ => model}/tactic/Action.ts (75%) rename front/{tactic/CourtObjects.ts => model/tactic/Ball.ts} (100%) rename front/{ => model}/tactic/Player.ts (84%) rename front/{ => model}/tactic/Tactic.ts (85%) create mode 100644 front/style/team_panel.css delete mode 100644 front/tactic/Team.ts create mode 100644 front/views/TeamPanel.tsx create mode 100644 src/App/Views/edit_team.html.twig delete mode 100755 src/Core/Data/Color.php delete mode 100755 src/Core/Data/MemberRole.php create mode 100644 src/Core/Data/User.php diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index e59bdb7..1e3d916 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -9,12 +9,13 @@ import { } from "react" import CourtPlayer from "./CourtPlayer" -import { Player } from "../../tactic/Player" -import { Action, ActionKind } from "../../tactic/Action" + +import { Player } from "../../model/tactic/Player" +import { Action, ActionKind } from "../../model/tactic/Action" import ArrowAction from "../actions/ArrowAction" import { middlePos, ratioWithinBase } from "../arrows/Pos" import BallAction from "../actions/BallAction" -import { CourtObject } from "../../tactic/CourtObjects" +import { CourtObject } from "../../model/tactic/Ball" import { contains } from "../arrows/Box" import { CourtAction } from "../../views/editor/CourtAction" @@ -255,7 +256,7 @@ export function BasketCourt({ /> ) } - throw new Error("unknown court object", object.type) + throw new Error("unknown court object" + object.type) })} {previewAction && ( diff --git a/front/components/editor/CourtBall.tsx b/front/components/editor/CourtBall.tsx index 9ba5ae5..b1fa1d0 100644 --- a/front/components/editor/CourtBall.tsx +++ b/front/components/editor/CourtBall.tsx @@ -1,7 +1,7 @@ import React, { useRef } from "react" import Draggable from "react-draggable" import { BallPiece } from "./BallPiece" -import { Ball } from "../../tactic/CourtObjects" +import { Ball } from "../../model/tactic/Ball" export interface CourtBallProps { onMoved: (rect: DOMRect) => void diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index 3d5ffde..c0c94d5 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -2,7 +2,7 @@ import { ReactNode, RefObject, useRef } from "react" import "../../style/player.css" import Draggable from "react-draggable" import { PlayerPiece } from "./PlayerPiece" -import { Player } from "../../tactic/Player" +import { Player } from "../../model/tactic/Player" import { NULL_POS, ratioWithinBase } from "../arrows/Pos" export interface PlayerProps { diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx index a1b5e74..1b52ff8 100644 --- a/front/components/editor/PlayerPiece.tsx +++ b/front/components/editor/PlayerPiece.tsx @@ -1,8 +1,8 @@ import "../../style/player.css" -import { Team } from "../../tactic/Team" +import { PlayerTeam } from "../../model/tactic/Player" export interface PlayerPieceProps { - team: Team + team: PlayerTeam text: string hasBall: boolean } diff --git a/front/model/Team.ts b/front/model/Team.ts new file mode 100644 index 0000000..99d530b --- /dev/null +++ b/front/model/Team.ts @@ -0,0 +1,19 @@ +import {User} from "./User"; + +export interface TeamInfo { + id: number + name: string + picture: string + mainColor: string + secondColor: string +} + +export interface Team { + info: TeamInfo + members: Member[] +} + +export interface Member { + user: User + role: string +} diff --git a/front/model/User.ts b/front/model/User.ts new file mode 100644 index 0000000..36bbb67 --- /dev/null +++ b/front/model/User.ts @@ -0,0 +1,6 @@ +export interface User { + id: number + name: string + email: string + profilePicture: string +} diff --git a/front/tactic/Action.ts b/front/model/tactic/Action.ts similarity index 75% rename from front/tactic/Action.ts rename to front/model/tactic/Action.ts index d66f375..0b5aee5 100644 --- a/front/tactic/Action.ts +++ b/front/model/tactic/Action.ts @@ -1,5 +1,5 @@ -import { Pos } from "../components/arrows/Pos" -import { Segment } from "../components/arrows/BendableArrow" +import { Pos } from "../../components/arrows/Pos" +import { Segment } from "../../components/arrows/BendableArrow" import { PlayerId } from "./Player" export enum ActionKind { diff --git a/front/tactic/CourtObjects.ts b/front/model/tactic/Ball.ts similarity index 100% rename from front/tactic/CourtObjects.ts rename to front/model/tactic/Ball.ts diff --git a/front/tactic/Player.ts b/front/model/tactic/Player.ts similarity index 84% rename from front/tactic/Player.ts rename to front/model/tactic/Player.ts index 1d71d8a..f94d6bf 100644 --- a/front/tactic/Player.ts +++ b/front/model/tactic/Player.ts @@ -1,14 +1,17 @@ -import { Team } from "./Team" - export type PlayerId = string +export enum PlayerTeam { + Allies = "allies", + Opponents = "opponents", +} + export interface Player { readonly id: PlayerId /** * the player's team * */ - readonly team: Team + readonly team: PlayerTeam /** * player's role diff --git a/front/tactic/Tactic.ts b/front/model/tactic/Tactic.ts similarity index 85% rename from front/tactic/Tactic.ts rename to front/model/tactic/Tactic.ts index 296c339..2eab85b 100644 --- a/front/tactic/Tactic.ts +++ b/front/model/tactic/Tactic.ts @@ -1,5 +1,5 @@ import { Player } from "./Player" -import { CourtObject } from "./CourtObjects" +import { CourtObject } from "./Ball" import { Action } from "./Action" export interface Tactic { diff --git a/front/style/team_panel.css b/front/style/team_panel.css new file mode 100644 index 0000000..6e48795 --- /dev/null +++ b/front/style/team_panel.css @@ -0,0 +1,135 @@ +#main-div { + display: flex; + flex-direction: column; + align-items: center; + height: 100%; +} + +#main-div header { + display: flex; + justify-content: center; + background-color: #525252; + width: 100%; + margin-bottom: 5px; +} + +header h1 a { + color: orange; + text-decoration: none; + font-size: 1.5em; +} + +.square { + width: 50px; + height: 50px; + border: 2px white solid; +} + +#team-info { + display: flex; + flex-direction: column; + align-items: center; + width: 60%; + background-color: #8f8f8f; + padding-bottom: 10px; + border-radius: 10px; +} + +#first-part { + display: flex; + flex-direction: column; + align-items: center; +} + +#team-name { + font-size: 2.8em; +} + +#colors { + display: flex; + flex-direction: column; +} +.color { + flex-direction: row; + justify-content: space-between; +} + +#colorsTitle { + width: 110%; + display: flex; + flex-direction: row; + justify-content: space-between; + font-size: 1.3em; + color: white; +} + +#actual-colors { + display: flex; + flex-direction: row; + justify-content: space-around; +} + +#logo { + aspect-ratio: 3/2; + object-fit: contain; + max-width: 70%; + max-height: 70%; +} + +#delete { + border-radius: 10px; + background-color: red; + color: white; + margin-top: 10px; + margin-bottom: 10px; + margin-right: 5px; +} + +#edit { + border-radius: 10px; + background-color: orange; + color: white; + margin-top: 10px; + margin-bottom: 10px; +} + +#head-members { + width: 33%; + display: flex; + flex-direction: row; + justify-content: space-evenly; +} + +#add-member { + height: 30px; + aspect-ratio: 1/1; + border-radius: 100%; + align-self: center; +} + +#members { + display: flex; + flex-direction: column; + background-color: #bcbcbc; + width: 60%; + align-items: center; + justify-content: space-around; + border-radius: 10px; +} + +.member { + width: 60%; + background-color: white; + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: center; + border-radius: 10px; + margin-top: 5px; + margin-bottom: 5px; +} + +#profile-picture { + height: 40px; + width: 40px; +} diff --git a/front/tactic/Team.ts b/front/tactic/Team.ts deleted file mode 100644 index 5b35943..0000000 --- a/front/tactic/Team.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum Team { - Allies = "allies", - Opponents = "opponents", -} diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 69c1eca..e1f0edd 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -16,22 +16,22 @@ import { BallPiece } from "../components/editor/BallPiece" import { Rack } from "../components/Rack" import { PlayerPiece } from "../components/editor/PlayerPiece" -import { Player } from "../tactic/Player" +import { Player } from "../model/tactic/Player" -import { Tactic, TacticContent } from "../tactic/Tactic" +import { Tactic, TacticContent } from "../model/tactic/Tactic" import { fetchAPI } from "../Fetcher" -import { Team } from "../tactic/Team" +import { PlayerTeam } from "../model/tactic/Player" import SavingState, { SaveState, SaveStates, } from "../components/editor/SavingState" -import { CourtObject } from "../tactic/CourtObjects" +import { CourtObject } from "../model/tactic/Ball" import { CourtAction } from "./editor/CourtAction" import { BasketCourt } from "../components/editor/BasketCourt" import { ratioWithinBase } from "../components/arrows/Pos" -import { Action, ActionKind } from "../tactic/Action" +import { Action, ActionKind } from "../model/tactic/Action" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -58,7 +58,7 @@ export interface EditorProps { * information about a player that is into a rack */ interface RackedPlayer { - team: Team + team: PlayerTeam key: string } @@ -134,10 +134,10 @@ function EditorView({ ) const [allies, setAllies] = useState( - getRackPlayers(Team.Allies, content.players), + getRackPlayers(PlayerTeam.Allies, content.players), ) const [opponents, setOpponents] = useState( - getRackPlayers(Team.Opponents, content.players), + getRackPlayers(PlayerTeam.Opponents, content.players), ) const [objects, setObjects] = useState( @@ -222,7 +222,7 @@ function EditorView({ break default: - throw new Error("unknown court object ", rackedObject.key) + throw new Error("unknown court object " + rackedObject.key) } setContent((content) => { @@ -357,10 +357,10 @@ function EditorView({ })) let setter switch (player.team) { - case Team.Opponents: + case PlayerTeam.Opponents: setter = setOpponents break - case Team.Allies: + case PlayerTeam.Allies: setter = setAllies } if (player.hasBall) { @@ -539,7 +539,7 @@ function renderCourtObject(courtObject: RackedCourtObject) { if (courtObject.key == "ball") { return } - throw new Error("unknown racked court object ", courtObject.key) + throw new Error("unknown racked court object " + courtObject.key) } function Court({ courtType }: { courtType: string }) { @@ -554,7 +554,7 @@ function Court({ courtType }: { courtType: string }) { ) } -function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] { +function getRackPlayers(team: PlayerTeam, players: Player[]): RackedPlayer[] { return ["1", "2", "3", "4", "5"] .filter( (role) => diff --git a/front/views/Home.tsx b/front/views/Home.tsx index 44803a3..8be885d 100644 --- a/front/views/Home.tsx +++ b/front/views/Home.tsx @@ -162,7 +162,7 @@ function TableData({ allTactics }: { allTactics: Tactic[] }) { function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) { let data if (allTactics.length == 0) { - data =

    Aucune tactique créé !

    + data =

    Aucune tactique créée !

    } else { data = } diff --git a/front/views/TeamPanel.tsx b/front/views/TeamPanel.tsx new file mode 100644 index 0000000..709d7f2 --- /dev/null +++ b/front/views/TeamPanel.tsx @@ -0,0 +1,169 @@ +import "../style/team_panel.css" +import { BASE } from "../Constants" +import { Team, TeamInfo, Member } from "../model/Team" +import { User } from "../model/User" + +export default function TeamPanel({ + isCoach, + team, + currentUserId, +}: { + isCoach: boolean + team: Team + currentUserId: number +}) { + return ( +
    +
    +

    + IQBall +

    +
    + + + {isCoach && } + + +
    + ) +} + +function TeamDisplay({ team }: { team: TeamInfo }) { + return ( +
    +
    +

    {team.name}

    + +
    +
    +
    +

    Couleur principale

    +

    Couleur secondaire

    +
    +
    + + +
    +
    +
    + ) +} + +function ColorDisplay({ color }: { color: string }) { + return
    +} + +function CoachOptions({ id }: { id: number }) { + return ( +
    + + +
    + ) +} + +function MembersDisplay({ + members, + isCoach, + idTeam, + currentUserId, +}: { + members: Member[] + isCoach: boolean + idTeam: number + currentUserId: number +}) { + const listMember = members.map((member) => ( + + )) + return ( +
    +
    +

    Membres :

    + {isCoach && ( + + )} +
    + {listMember} +
    + ) +} + +function MemberDisplay({ + member, + isCoach, + idTeam, + currentUserId, +}: { + member: Member + isCoach: boolean + idTeam: number + currentUserId: number +}) { + return ( +
    + Photo de profile +

    {member.user.name}

    +

    {member.role}

    +

    {member.user.email}

    + {isCoach && currentUserId !== member.user.id && ( + + )} + {isCoach && currentUserId == member.user.id && ( + + )} +
    + ) +} diff --git a/front/views/editor/CourtAction.tsx b/front/views/editor/CourtAction.tsx index b4028ff..de33224 100644 --- a/front/views/editor/CourtAction.tsx +++ b/front/views/editor/CourtAction.tsx @@ -1,4 +1,4 @@ -import { Action, ActionKind } from "../../tactic/Action" +import { Action, ActionKind } from "../../model/tactic/Action" import BendableArrow from "../../components/arrows/BendableArrow" import { RefObject } from "react" import { MoveToHead, ScreenHead } from "../../components/actions/ArrowAction" diff --git a/public/api/index.php b/public/api/index.php index 5734571..da25013 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -51,7 +51,6 @@ function tryGetAuthorization(): ?Account { $session = PhpSessionHandle::init(); return $session->getAccount(); } - $token = $headers['Authorization']; $gateway = new AccountGateway(new Connection(get_database())); return $gateway->getAccountFromToken($token); diff --git a/public/index.php b/public/index.php index 935e2f2..82dd37f 100644 --- a/public/index.php +++ b/public/index.php @@ -102,10 +102,13 @@ function getRoutes(): AltoRouter { $ar->map("GET", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->displayListTeamByName($s))); $ar->map("POST", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->listTeamByName($_POST, $s))); $ar->map("GET", "/team/[i:id]", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->displayTeam($id, $s))); - $ar->map("GET", "/team/members/add", Action::auth(fn(SessionHandle $s) => getTeamController()->displayAddMember($s))); - $ar->map("POST", "/team/members/add", Action::auth(fn(SessionHandle $s) => getTeamController()->addMember($_POST, $s))); - $ar->map("GET", "/team/members/remove", Action::auth(fn(SessionHandle $s) => getTeamController()->displayDeleteMember($s))); - $ar->map("POST", "/team/members/remove", Action::auth(fn(SessionHandle $s) => getTeamController()->deleteMember($_POST, $s))); + $ar->map("GET", "/team/[i:id]/delete", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->deleteTeamById($id, $s))); + $ar->map("GET", "/team/[i:id]/addMember", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->displayAddMember($id, $s))); + $ar->map("POST", "/team/[i:id]/addMember", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->addMember($id, $_POST, $s))); + $ar->map("GET", "/team/[i:idTeam]/remove/[i:idMember]", Action::auth(fn(int $idTeam, int $idMember, SessionHandle $s) => getTeamController()->deleteMember($idTeam, $idMember, $s))); + $ar->map("GET", "/team/[i:id]/edit", Action::auth(fn(int $idTeam, SessionHandle $s) => getTeamController()->displayEditTeam($idTeam, $s))); + $ar->map("POST", "/team/[i:id]/edit", Action::auth(fn(int $idTeam, SessionHandle $s) => getTeamController()->editTeam($idTeam, $_POST, $s))); + return $ar; } diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 633081f..0d157d9 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -10,7 +10,8 @@ CREATE TABLE Account email varchar UNIQUE NOT NULL, username varchar NOT NULL, token varchar UNIQUE NOT NULL, - hash varchar NOT NULL + hash varchar NOT NULL, + profilePicture varchar NOT NULL ); CREATE TABLE Tactic diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 79e766c..a116add 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -36,7 +36,7 @@ class APITacticController { "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], ], function (HttpRequest $request) use ($tactic_id, $account) { - $failures = $this->model->updateName($tactic_id, $request["name"], $account->getId()); + $failures = $this->model->updateName($tactic_id, $request["name"], $account->getUser()->getId()); if (!empty($failures)) { //TODO find a system to handle Unauthorized error codes more easily from failures. diff --git a/src/App/Controller/EditorController.php b/src/App/Controller/EditorController.php index 5561590..4bdcfae 100644 --- a/src/App/Controller/EditorController.php +++ b/src/App/Controller/EditorController.php @@ -63,7 +63,7 @@ class EditorController { return $this->openTestEditor($type); } - $tactic = $this->model->makeNewDefault($session->getAccount()->getId(), $type); + $tactic = $this->model->makeNewDefault($session->getAccount()->getUser()->getId(), $type); return $this->openEditorFor($tactic); } @@ -76,7 +76,7 @@ class EditorController { public function openEditor(int $id, SessionHandle $session): ViewHttpResponse { $tactic = $this->model->get($id); - $failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getId()); + $failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getUser()->getId()); if ($failure != null) { return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); diff --git a/src/App/Controller/TeamController.php b/src/App/Controller/TeamController.php index b2c0ea9..4ab3fd7 100644 --- a/src/App/Controller/TeamController.php +++ b/src/App/Controller/TeamController.php @@ -4,10 +4,13 @@ namespace IQBall\App\Controller; use IQBall\App\Session\SessionHandle; use IQBall\App\ViewHttpResponse; +use IQBall\Core\Data\Account; +use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Model\TeamModel; use IQBall\Core\Validation\FieldValidationFail; +use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\Validators; class TeamController { @@ -28,16 +31,6 @@ class TeamController { return ViewHttpResponse::twig("insert_team.html.twig", []); } - - /** - * @param SessionHandle $session - * @return ViewHttpResponse the team panel to add a member - */ - public function displayAddMember(SessionHandle $session): ViewHttpResponse { - return ViewHttpResponse::twig("add_member.html.twig", []); - } - - /** * @param SessionHandle $session * @return ViewHttpResponse the team panel to delete a member @@ -70,7 +63,8 @@ class TeamController { return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]); } $teamId = $this->model->createTeam($request['name'], $request['picture'], $request['main_color'], $request['second_color']); - return $this->displayTeam($teamId, $session); + $this->model->addMember($session->getAccount()->getUser()->getEmail(), $teamId, 'COACH'); + return HttpResponse::redirect('/team/' . $teamId); } /** @@ -98,58 +92,155 @@ class TeamController { return ViewHttpResponse::twig('list_team_by_name.html.twig', ['bad_field' => $badField]); } - $teams = $this->model->listByName($request['name']); + $teams = $this->model->listByName($request['name'], $session->getAccount()->getUser()->getId()); if (empty($teams)) { return ViewHttpResponse::twig('display_teams.html.twig', []); } - return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $teams]); } /** + * Delete a team with its id + * @param int $id + * @param SessionHandle $session + * @return HttpResponse + */ + public function deleteTeamById(int $id, SessionHandle $session): HttpResponse { + $a = $session->getAccount(); + $ret = $this->model->deleteTeam($a->getUser()->getEmail(), $id); + if($ret != 0) { + return ViewHttpResponse::twig('display_team.html.twig', ['notDeleted' => true]); + } + return HttpResponse::redirect('/'); + } + + /** + * Display a team with its id * @param int $id * @param SessionHandle $session * @return ViewHttpResponse a view that displays given team information */ public function displayTeam(int $id, SessionHandle $session): ViewHttpResponse { - $result = $this->model->getTeam($id); - return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]); + $result = $this->model->getTeam($id, $session->getAccount()->getUser()->getId()); + if($result == null) { + return ViewHttpResponse::twig('error.html.twig', [ + 'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette équipe.")], + ], HttpCodes::FORBIDDEN); + } + $role = $this->model->isCoach($id, $session->getAccount()->getUser()->getEmail()); + + return ViewHttpResponse::react( + 'views/TeamPanel.tsx', + [ + 'team' => [ + "info" => $result->getInfo(), + "members" => $result->listMembers(), + ], + 'isCoach' => $role, + 'currentUserId' => $session->getAccount()->getUser()->getId()] + ); + } + + /** + * @param int $idTeam + * @param SessionHandle $session + * @return ViewHttpResponse the team panel to add a member + */ + public function displayAddMember(int $idTeam, SessionHandle $session): ViewHttpResponse { + return ViewHttpResponse::twig("add_member.html.twig", ['idTeam' => $idTeam]); } /** * add a member to a team + * @param int $idTeam * @param array $request * @param SessionHandle $session * @return HttpResponse */ - public function addMember(array $request, SessionHandle $session): HttpResponse { + public function addMember(int $idTeam, array $request, SessionHandle $session): HttpResponse { $errors = []; - + if(!$this->model->isCoach($idTeam, $session->getAccount()->getUser()->getEmail())) { + return ViewHttpResponse::twig('error.html.twig', [ + 'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette action pour cette équipe.")], + ], HttpCodes::FORBIDDEN); + } $request = HttpRequest::from($request, $errors, [ - "team" => [Validators::isInteger()], "email" => [Validators::email(), Validators::lenBetween(5, 256)], ]); + if(!empty($errors)) { + return ViewHttpResponse::twig('add_member.html.twig', ['badEmail' => true,'idTeam' => $idTeam]); + } + $ret = $this->model->addMember($request['email'], $idTeam, $request['role']); + + switch($ret) { + case -1: + return ViewHttpResponse::twig('add_member.html.twig', ['notFound' => true,'idTeam' => $idTeam]); + case -2: + return ViewHttpResponse::twig('add_member.html.twig', ['alreadyExisting' => true,'idTeam' => $idTeam]); + default: + return HttpResponse::redirect('/team/' . $idTeam); + } + } - $teamId = intval($request['team']); - $this->model->addMember($request['email'], $teamId, $request['role']); + /** + * remove a member from a team with their ids + * @param int $idTeam + * @param int $idMember + * @param SessionHandle $session + * @return HttpResponse + */ + public function deleteMember(int $idTeam, int $idMember, SessionHandle $session): HttpResponse { + if(!$this->model->isCoach($idTeam, $session->getAccount()->getUser()->getEmail())) { + return ViewHttpResponse::twig('error.html.twig', [ + 'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette action pour cette équipe.")], + ], HttpCodes::FORBIDDEN); + } + $teamId = $this->model->deleteMember($idMember, $idTeam); + if($teamId == -1 || $session->getAccount()->getUser()->getId() == $idMember) { + return HttpResponse::redirect('/'); + } return $this->displayTeam($teamId, $session); } /** - * remove a member from a team - * @param array $request + * @param int $idTeam * @param SessionHandle $session - * @return HttpResponse + * @return ViewHttpResponse */ - public function deleteMember(array $request, SessionHandle $session): HttpResponse { - $errors = []; + public function displayEditTeam(int $idTeam, SessionHandle $session): ViewHttpResponse { + return ViewHttpResponse::twig("edit_team.html.twig", ['team' => $this->model->getTeam($idTeam, $session->getAccount()->getUser()->getId())]); + } - $request = HttpRequest::from($request, $errors, [ - "team" => [Validators::isInteger()], - "email" => [Validators::email(), Validators::lenBetween(5, 256)], + /** + * @param int $idTeam + * @param array $request + * @param SessionHandle $session + * @return HttpResponse + */ + public function editTeam(int $idTeam, array $request, SessionHandle $session): HttpResponse { + if(!$this->model->isCoach($idTeam, $session->getAccount()->getUser()->getEmail())) { + return ViewHttpResponse::twig('error.html.twig', [ + 'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette action pour cette équipe.")], + ], HttpCodes::FORBIDDEN); + } + $failures = []; + $request = HttpRequest::from($request, $failures, [ + "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], + "main_color" => [Validators::hexColor()], + "second_color" => [Validators::hexColor()], + "picture" => [Validators::isURL()], ]); - - return $this->displayTeam($this->model->deleteMember($request['email'], intval($request['team'])), $session); + if (!empty($failures)) { + $badFields = []; + foreach ($failures as $e) { + if ($e instanceof FieldValidationFail) { + $badFields[] = $e->getFieldName(); + } + } + return ViewHttpResponse::twig('edit_team.html.twig', ['bad_fields' => $badFields]); + } + $this->model->editTeam($idTeam, $request['name'], $request['picture'], $request['main_color'], $request['second_color']); + return HttpResponse::redirect('/team/' . $idTeam); } } diff --git a/src/App/Controller/UserController.php b/src/App/Controller/UserController.php index e33ee61..2718843 100644 --- a/src/App/Controller/UserController.php +++ b/src/App/Controller/UserController.php @@ -16,6 +16,7 @@ class UserController { /** * @param TacticModel $tactics + * @param TeamModel|null $teams */ public function __construct(TacticModel $tactics, ?TeamModel $teams = null) { $this->tactics = $tactics; @@ -28,12 +29,15 @@ class UserController { */ public function home(SessionHandle $session): ViewHttpResponse { $limitNbTactics = 5; - $lastTactics = $this->tactics->getLast($limitNbTactics, $session->getAccount()->getId()); - $allTactics = $this->tactics->getAll($session->getAccount()->getId()); - $name = $session->getAccount()->getName(); + + $user = $session->getAccount()->getUser(); + + $lastTactics = $this->tactics->getLast($limitNbTactics, $user->getId()); + $allTactics = $this->tactics->getAll($user->getId()); + $name = $user->getName(); if ($this->teams != null) { - $teams = $this->teams->getAll($session->getAccount()->getId()); + $teams = $this->teams->getAll($user->getId()); } else { $teams = []; } @@ -45,11 +49,7 @@ class UserController { "username" => $name, ]); } - - public function homeTwig(SessionHandle $session): ViewHttpResponse { - return ViewHttpResponse::twig("home.twig", []); - } - + /** * @return ViewHttpResponse account settings page */ diff --git a/src/App/Controller/VisualizerController.php b/src/App/Controller/VisualizerController.php index 631468e..946f6d0 100644 --- a/src/App/Controller/VisualizerController.php +++ b/src/App/Controller/VisualizerController.php @@ -28,7 +28,7 @@ class VisualizerController { public function openVisualizer(int $id, SessionHandle $session): HttpResponse { $tactic = $this->tacticModel->get($id); - $failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getId()); + $failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getUser()->getId()); if ($failure != null) { return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); diff --git a/src/App/Views/add_member.html.twig b/src/App/Views/add_member.html.twig index c6bae0e..cfee16d 100644 --- a/src/App/Views/add_member.html.twig +++ b/src/App/Views/add_member.html.twig @@ -67,28 +67,43 @@ display: flex; justify-content: space-between; } + + .failed{ + color: red; + } +
    +

    IQBall

    +

    Ajouter un membre à votre équipe

    -
    +
    - - - - + + + {% if badEmail %} +

    Email invalide

    + {% endif %} + {%if notFound %} +

    Cette personne n'a pas été trouvé

    + {% endif %} + {% if alreadyExisting %} +

    Cette personne est déjà dans l'équipe

    + {% endif %} +
    Rôle du membre dans l'équipe :
    - +
    - +
    diff --git a/src/App/Views/display_team.html.twig b/src/App/Views/display_team.html.twig index 7f23b8b..8928e84 100644 --- a/src/App/Views/display_team.html.twig +++ b/src/App/Views/display_team.html.twig @@ -11,10 +11,6 @@ align-items: center; } - section { - width: 60%; - } - .square { width: 50px; height: 50px; @@ -30,19 +26,17 @@ border: solid; } - .container { + section { background-color: #fff; display: flex; flex-direction: column; align-items: center; + width: 60%; } - .team { - border-color: darkgrey; - border-radius: 20px; - + #colors{ + flex-direction: row; } - .color { flex-direction: row; justify-content: space-between; @@ -53,6 +47,16 @@ width: 80px; } + #delete{ + border-radius:10px ; + background-color: red; + color: white; + } + + .player{ + flex-direction: row; + justify-content: space-evenly; + } @@ -61,13 +65,18 @@
    - -
    + {% if notDeleted %} + +

    Cette équipe ne peut être supprimée.

    +
    + {% endif %} +{% if team is defined %} +

    {{ team.getInfo().getName() }}

    -
    +

    Couleur principale :

    @@ -75,17 +84,26 @@
    - + {% if isCoach %} + + + {% endif %} {% for m in team.listMembers() %} -

    {{ m.getUserId() }}

    - {% if m.getRole().isCoach() %} +
    +

    {{ m.getUserId() }}

    + {% if m.getRole().isCoach() %}

    : Coach

    - {% else %} + {% else %}

    : Joueur

    - {% endif %} + {% endif %} +
    {% endfor %}
    - +{% else %} +
    +

    Cette équipe ne peut être affichée

    +
    +{% endif %}
    \ No newline at end of file diff --git a/src/App/Views/display_teams.html.twig b/src/App/Views/display_teams.html.twig index 1e1420a..3e3ab12 100644 --- a/src/App/Views/display_teams.html.twig +++ b/src/App/Views/display_teams.html.twig @@ -3,9 +3,37 @@ Twig view + - +
    +

    IQBall

    +
    +
    {% if teams is empty %}

    Aucune équipe n'a été trouvée

    @@ -22,12 +50,12 @@
    {% else %} {% for t in teams %} -
    -

    Nom de l'équipe : {{ t.name }}

    - logo de l'équipe +
    +

    Nom de l'équipe : {{ t.getName() }}

    + logo de l'équipe
    {% endfor %} {% endif %} - +
    \ No newline at end of file diff --git a/src/App/Views/edit_team.html.twig b/src/App/Views/edit_team.html.twig new file mode 100644 index 0000000..409d71a --- /dev/null +++ b/src/App/Views/edit_team.html.twig @@ -0,0 +1,81 @@ + + + + + Insertion view + + + + +
    +

    Modifier votre équipe

    + +
    + + + + + + + + +
    +
    + +
    + +
    + + + \ No newline at end of file diff --git a/src/App/Views/home.twig b/src/App/Views/home.twig index 7d8430e..0fc426a 100644 --- a/src/App/Views/home.twig +++ b/src/App/Views/home.twig @@ -74,7 +74,7 @@
    {% endfor %} {% else %} -

    Aucune équipe créé !

    +

    Aucune équipe créée !

    {% endif %}

    Mes strategies

    @@ -90,7 +90,7 @@
    {% endfor %} {% else %} -

    Aucune tactique créé !

    +

    Aucune tactique créée !

    {% endif %} diff --git a/src/App/Views/insert_team.html.twig b/src/App/Views/insert_team.html.twig index 65cd096..0c10114 100644 --- a/src/App/Views/insert_team.html.twig +++ b/src/App/Views/insert_team.html.twig @@ -54,7 +54,6 @@ background-color: #0056b3; } - @@ -68,7 +67,7 @@ - +
    diff --git a/src/App/Views/list_team_by_name.html.twig b/src/App/Views/list_team_by_name.html.twig index eca5e19..092a149 100644 --- a/src/App/Views/list_team_by_name.html.twig +++ b/src/App/Views/list_team_by_name.html.twig @@ -7,6 +7,9 @@ body { font-family: Arial, sans-serif; background-color: #f1f1f1; + display: flex; + flex-direction: column; + align-items: center; } .container { @@ -56,7 +59,9 @@ - +
    +

    IQBall

    +

    Chercher une équipe

    diff --git a/src/Core/Data/Account.php b/src/Core/Data/Account.php index 48b3e69..01f5406 100755 --- a/src/Core/Data/Account.php +++ b/src/Core/Data/Account.php @@ -8,54 +8,34 @@ namespace IQBall\Core\Data; * to share to other users, or non-needed public information */ class Account { - /** - * @var string $email account's mail address - */ - private string $email; - /** * @var string string token */ private string $token; /** - * @var string the account's username + * @var User contains all the account's "public" information */ - private string $name; - - /** - * @var int - */ - private int $id; - + private User $user; /** - * @param string $email - * @param string $name * @param string $token - * @param int $id + * @param User $user */ - public function __construct(string $email, string $name, string $token, int $id) { - $this->email = $email; - $this->name = $name; + public function __construct(string $token, User $user) { $this->token = $token; - $this->id = $id; - } - - public function getId(): int { - return $this->id; - } - - public function getEmail(): string { - return $this->email; + $this->user = $user; } public function getToken(): string { return $this->token; } - public function getName(): string { - return $this->name; + /** + * @return User + */ + public function getUser(): User { + return $this->user; } } diff --git a/src/Core/Data/Color.php b/src/Core/Data/Color.php deleted file mode 100755 index e0cd27c..0000000 --- a/src/Core/Data/Color.php +++ /dev/null @@ -1,44 +0,0 @@ -hex = $value; - } - - /** - * @return string - */ - public function getValue(): string { - return $this->hex; - } - - public static function from(string $value): Color { - $color = self::tryFrom($value); - if ($color == null) { - var_dump($value); - throw new InvalidArgumentException("The string is not an hexadecimal code"); - } - return $color; - } - - public static function tryFrom(string $value): ?Color { - if (!preg_match('/#(?:[0-9a-fA-F]{6})/', $value)) { - return null; - } - return new Color($value); - } -} diff --git a/src/Core/Data/Member.php b/src/Core/Data/Member.php index d68140c..30e4202 100755 --- a/src/Core/Data/Member.php +++ b/src/Core/Data/Member.php @@ -5,11 +5,8 @@ namespace IQBall\Core\Data; /** * information about a team member */ -class Member { - /** - * @var int The member's user account - */ - private int $userId; +class Member implements \JsonSerializable { + private User $user; /** * @var int The member's team id @@ -17,32 +14,25 @@ class Member { private int $teamId; /** - * @var MemberRole the member's role + * @var string the member's role */ - private MemberRole $role; + private string $role; /** - * @param int $userId - * @param MemberRole $role + * @param User $user + * @param int $teamId + * @param string $role */ - public function __construct(int $userId, int $teamId, MemberRole $role) { - $this->userId = $userId; + public function __construct(User $user, int $teamId, string $role) { + $this->user = $user; $this->teamId = $teamId; $this->role = $role; } - - /** - * @return int - */ - public function getUserId(): int { - return $this->userId; - } - /** - * @return MemberRole + * @return string */ - public function getRole(): MemberRole { + public function getRole(): string { return $this->role; } @@ -52,4 +42,16 @@ class Member { public function getTeamId(): int { return $this->teamId; } + + /** + * @return User + */ + public function getUser(): User { + return $this->user; + } + + + public function jsonSerialize() { + return get_object_vars($this); + } } diff --git a/src/Core/Data/MemberRole.php b/src/Core/Data/MemberRole.php deleted file mode 100755 index 9606c0b..0000000 --- a/src/Core/Data/MemberRole.php +++ /dev/null @@ -1,68 +0,0 @@ -isValid($val)) { - throw new InvalidArgumentException("Valeur du rôle invalide"); - } - $this->value = $val; - } - - public static function player(): MemberRole { - return new MemberRole(MemberRole::ROLE_PLAYER); - } - - public static function coach(): MemberRole { - return new MemberRole(MemberRole::ROLE_COACH); - } - - public function name(): string { - switch ($this->value) { - case self::ROLE_COACH: - return "COACH"; - case self::ROLE_PLAYER: - return "PLAYER"; - } - die("unreachable"); - } - - public static function fromName(string $name): ?MemberRole { - switch ($name) { - case "COACH": - return MemberRole::coach(); - case "PLAYER": - return MemberRole::player(); - default: - return null; - } - } - - private function isValid(int $val): bool { - return ($val <= self::MAX and $val >= self::MIN); - } - - public function isPlayer(): bool { - return ($this->value == self::ROLE_PLAYER); - } - - public function isCoach(): bool { - return ($this->value == self::ROLE_COACH); - } - -} diff --git a/src/Core/Data/Team.php b/src/Core/Data/Team.php index b8e7834..7adeb49 100755 --- a/src/Core/Data/Team.php +++ b/src/Core/Data/Team.php @@ -2,7 +2,7 @@ namespace IQBall\Core\Data; -class Team { +class Team implements \JsonSerializable { private TeamInfo $info; /** @@ -29,4 +29,10 @@ class Team { public function listMembers(): array { return $this->members; } + + public function jsonSerialize() { + return get_object_vars($this); + } + + } diff --git a/src/Core/Data/TeamInfo.php b/src/Core/Data/TeamInfo.php index 7affcea..0f741fe 100644 --- a/src/Core/Data/TeamInfo.php +++ b/src/Core/Data/TeamInfo.php @@ -2,21 +2,21 @@ namespace IQBall\Core\Data; -class TeamInfo { +class TeamInfo implements \JsonSerializable { private int $id; private string $name; private string $picture; - private Color $mainColor; - private Color $secondColor; + private string $mainColor; + private string $secondColor; /** * @param int $id * @param string $name * @param string $picture - * @param Color $mainColor - * @param Color $secondColor + * @param string $mainColor + * @param string $secondColor */ - public function __construct(int $id, string $name, string $picture, Color $mainColor, Color $secondColor) { + public function __construct(int $id, string $name, string $picture, string $mainColor, string $secondColor) { $this->id = $id; $this->name = $name; $this->picture = $picture; @@ -37,13 +37,17 @@ class TeamInfo { return $this->picture; } - public function getMainColor(): Color { + public function getMainColor(): string { return $this->mainColor; } - public function getSecondColor(): Color { + public function getSecondColor(): string { return $this->secondColor; } + public function jsonSerialize() { + return get_object_vars($this); + } + } diff --git a/src/Core/Data/User.php b/src/Core/Data/User.php new file mode 100644 index 0000000..71e0dd1 --- /dev/null +++ b/src/Core/Data/User.php @@ -0,0 +1,72 @@ +email = $email; + $this->name = $name; + $this->id = $id; + $this->profilePicture = $profilePicture; + } + + /** + * @return string + */ + public function getEmail(): string { + return $this->email; + } + + /** + * @return string + */ + public function getName(): string { + return $this->name; + } + + /** + * @return int + */ + public function getId(): int { + return $this->id; + } + + /** + * @return string + */ + public function getProfilePicture(): string { + return $this->profilePicture; + } + + public function jsonSerialize() { + return get_object_vars($this); + } +} diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php index 7740b57..a9c3e18 100644 --- a/src/Core/Gateway/AccountGateway.php +++ b/src/Core/Gateway/AccountGateway.php @@ -4,6 +4,7 @@ namespace IQBall\Core\Gateway; use IQBall\Core\Connection; use IQBall\Core\Data\Account; +use IQBall\Core\Data\User; use PDO; class AccountGateway { @@ -16,13 +17,13 @@ class AccountGateway { $this->con = $con; } - - public function insertAccount(string $name, string $email, string $token, string $hash): int { - $this->con->exec("INSERT INTO Account(username, hash, email, token) VALUES (:username,:hash,:email,:token)", [ + public function insertAccount(string $name, string $email, string $token, string $hash, string $profilePicture): int { + $this->con->exec("INSERT INTO Account(username, hash, email, token,profilePicture) VALUES (:username,:hash,:email,:token,:profilePic)", [ ':username' => [$name, PDO::PARAM_STR], ':hash' => [$hash, PDO::PARAM_STR], ':email' => [$email, PDO::PARAM_STR], ':token' => [$token, PDO::PARAM_STR], + ':profilePic' => [$profilePicture, PDO::PARAM_STR], ]); return intval($this->con->lastInsertId()); } @@ -65,7 +66,7 @@ class AccountGateway { return null; } - return new Account($email, $acc["username"], $acc["token"], $acc["id"]); + return new Account($acc["token"], new User($email, $acc["username"], $acc["id"], $acc["profilePicture"])); } /** @@ -78,7 +79,7 @@ class AccountGateway { return null; } - return new Account($acc["email"], $acc["username"], $acc["token"], $acc["id"]); + return new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profilePicture"])); } diff --git a/src/Core/Gateway/MemberGateway.php b/src/Core/Gateway/MemberGateway.php index 999bf10..a5116e8 100644 --- a/src/Core/Gateway/MemberGateway.php +++ b/src/Core/Gateway/MemberGateway.php @@ -4,7 +4,7 @@ namespace IQBall\Core\Gateway; use IQBall\Core\Connection; use IQBall\Core\Data\Member; -use IQBall\Core\Data\MemberRole; +use IQBall\Core\Data\User; use PDO; class MemberGateway { @@ -41,13 +41,12 @@ class MemberGateway { */ public function getMembersOfTeam(int $teamId): array { $rows = $this->con->fetch( - "SELECT a.id,m.role,a.email,a.username FROM Account a,Team t,Member m WHERE t.id = :id AND m.id_team = t.id AND m.id_user = a.id", + "SELECT a.id,a.email,a.username,a.profilePicture,m.role FROM Account a,team t,Member m WHERE t.id = :id AND m.id_team = t.id AND m.id_user = a.id", [ ":id" => [$teamId, PDO::PARAM_INT], ] ); - - return array_map(fn($row) => new Member($row['id_user'], $row['id_team'], MemberRole::fromName($row['role'])), $rows); + return array_map(fn($row) => new Member(new User($row['email'], $row['username'], $row['id'], $row['profilePicture']), $teamId, $row['role']), $rows); } /** @@ -66,4 +65,36 @@ class MemberGateway { ); } + /** + * @param string $email + * @param int $idTeam + * @return bool + */ + public function isCoach(string $email, int $idTeam): bool { + $result = $this->con->fetch( + "SELECT role FROM Member WHERE id_team=:team AND id_user = (SELECT id FROM Account WHERE email=:email)", + [ + "team" => [$idTeam, PDO::PARAM_INT], + "email" => [$email, PDO::PARAM_STR], + ] + )[0]['role']; + + return $result == 'COACH'; + } + + /** + * @param int $idTeam + * @param int $idCurrentUser + * @return bool + */ + public function isMemberOfTeam(int $idTeam, int $idCurrentUser): bool { + $result = $this->con->fetch( + "SELECT id_user FROM Member WHERE id_team = :team AND id_user = :user", + [ + "team" => [$idTeam, PDO::PARAM_INT], + "user" => [$idCurrentUser, PDO::PARAM_INT], + ] + ); + return !empty($result); + } } diff --git a/src/Core/Gateway/TeamGateway.php b/src/Core/Gateway/TeamGateway.php index bc1da94..fe0b2bb 100644 --- a/src/Core/Gateway/TeamGateway.php +++ b/src/Core/Gateway/TeamGateway.php @@ -3,7 +3,6 @@ namespace IQBall\Core\Gateway; use IQBall\Core\Connection; -use IQBall\Core\Data\Color; use IQBall\Core\Data\TeamInfo; use PDO; @@ -23,7 +22,7 @@ class TeamGateway { */ public function insert(string $name, string $picture, string $mainColor, string $secondColor): int { $this->con->exec( - "INSERT INTO Team(name, picture, main_color, second_color) VALUES (:team_name , :picture, :main_color, :second_color)", + "INSERT INTO team(name, picture, main_color, second_color) VALUES (:team_name , :picture, :main_color, :second_color)", [ ":team_name" => [$name, PDO::PARAM_STR], ":picture" => [$picture, PDO::PARAM_STR], @@ -34,38 +33,37 @@ class TeamGateway { return intval($this->con->lastInsertId()); } - /** * @param string $name + * @param int $id * @return TeamInfo[] */ - public function listByName(string $name): array { + public function listByName(string $name, int $id): array { $result = $this->con->fetch( - "SELECT * FROM Team WHERE name LIKE '%' || :name || '%'", + "SELECT t.* FROM team t, Member m WHERE t.name LIKE '%' || :name || '%' AND t.id=m.id_team AND m.id_user=:id", [ ":name" => [$name, PDO::PARAM_STR], + "id" => [$id, PDO::PARAM_INT], ] ); - - return array_map(fn($row) => new TeamInfo($row['id'], $row['name'], $row['picture'], Color::from($row['main_color']), Color::from($row['second_color'])), $result); + return array_map(fn($row) => new TeamInfo($row['id'], $row['name'], $row['picture'], $row['main_color'], $row['second_color']), $result); } /** * @param int $id - * @return TeamInfo + * @return TeamInfo|null */ public function getTeamById(int $id): ?TeamInfo { $row = $this->con->fetch( - "SELECT * FROM Team WHERE id = :id", + "SELECT * FROM team WHERE id = :id", [ - ":id" => [$id, PDO::PARAM_INT], - ] + ":id" => [$id, PDO::PARAM_INT], + ] )[0] ?? null; if ($row == null) { return null; } - - return new TeamInfo($row['id'], $row['name'], $row['picture'], Color::from($row['main_color']), Color::from($row['second_color'])); + return new TeamInfo($row['id'], $row['name'], $row['picture'], $row['main_color'], $row['second_color']); } /** @@ -74,16 +72,60 @@ class TeamGateway { */ public function getTeamIdByName(string $name): ?int { return $this->con->fetch( - "SELECT id FROM Team WHERE name = :name", + "SELECT id FROM team WHERE name = :name", [ - ":name" => [$name, PDO::PARAM_INT], - ] + ":name" => [$name, PDO::PARAM_INT], + ] )[0]['id'] ?? null; } + /** + * @param int $idTeam + */ + public function deleteTeam(int $idTeam): void { + $this->con->exec( + "DELETE FROM Member WHERE id_team=:team", + [ + "team" => [$idTeam, PDO::PARAM_INT], + ] + ); + $this->con->exec( + "DELETE FROM TEAM WHERE id=:team", + [ + "team" => [$idTeam, PDO::PARAM_INT], + ] + ); + } + + /** + * @param int $idTeam + * @param string $newName + * @param string $newPicture + * @param string $newMainColor + * @param string $newSecondColor + * @return void + */ + public function editTeam(int $idTeam, string $newName, string $newPicture, string $newMainColor, string $newSecondColor) + { + $this->con->exec( + "UPDATE team + SET name = :newName, + picture = :newPicture, + main_color = :newMainColor, + second_color = :newSecondColor + WHERE id = :team", + [ + "team" => [$idTeam, PDO::PARAM_INT], + "newName" => [$newName, PDO::PARAM_STR], + "newPicture" => [$newPicture, PDO::PARAM_STR], + "newMainColor" => [$newMainColor, PDO::PARAM_STR], + "newSecondColor" => [$newSecondColor, PDO::PARAM_STR], + ] + ); + + } /** * Get all the user's teams - * * @param integer $user * @return array> */ diff --git a/src/Core/Model/AuthModel.php b/src/Core/Model/AuthModel.php index 929eb99..bc29248 100644 --- a/src/Core/Model/AuthModel.php +++ b/src/Core/Model/AuthModel.php @@ -2,13 +2,16 @@ namespace IQBall\Core\Model; +use Exception; use IQBall\Core\Data\Account; +use IQBall\Core\Data\User; use IQBall\Core\Gateway\AccountGateway; use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\ValidationFail; class AuthModel { private AccountGateway $gateway; + private const DEFAULT_PROFILE_PICTURE = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png"; /** * @param AccountGateway $gateway @@ -17,7 +20,6 @@ class AuthModel { $this->gateway = $gateway; } - /** * @param string $username * @param string $password @@ -25,6 +27,7 @@ class AuthModel { * @param string $email * @param ValidationFail[] $failures * @return Account|null the registered account or null if failures occurred + * @throws Exception */ public function register(string $username, string $password, string $confirmPassword, string $email, array &$failures): ?Account { @@ -43,13 +46,14 @@ class AuthModel { $hash = password_hash($password, PASSWORD_DEFAULT); $token = $this->generateToken(); - $accountId = $this->gateway->insertAccount($username, $email, $token, $hash); - return new Account($email, $username, $token, $accountId); + $accountId = $this->gateway->insertAccount($username, $email, $token, $hash, self::DEFAULT_PROFILE_PICTURE); + return new Account($token, new User($email, $username, $accountId, self::DEFAULT_PROFILE_PICTURE)); } /** * Generate a random base 64 string * @return string + * @throws Exception */ private function generateToken(): string { return base64_encode(random_bytes(64)); @@ -70,5 +74,4 @@ class AuthModel { return $this->gateway->getAccountFromMail($email); } - } diff --git a/src/Core/Model/TeamModel.php b/src/Core/Model/TeamModel.php index a5ba84b..d6c97ce 100644 --- a/src/Core/Model/TeamModel.php +++ b/src/Core/Model/TeamModel.php @@ -2,7 +2,6 @@ namespace IQBall\Core\Model; -use IQBall\Core\Data\Color; use IQBall\Core\Data\Team; use IQBall\Core\Data\TeamInfo; use IQBall\Core\Gateway\AccountGateway; @@ -26,6 +25,7 @@ class TeamModel { } /** + * Create a team * @param string $name * @param string $picture * @param string $mainColor @@ -37,48 +37,100 @@ class TeamModel { } /** - * adds a member to a team + * add a member to a team * @param string $mail * @param int $teamId * @param string $role - * @return void + * @return int */ - public function addMember(string $mail, int $teamId, string $role): void { - $userId = $this->users->getAccountFromMail($mail)->getId(); - $this->members->insert($teamId, $userId, $role); + public function addMember(string $mail, int $teamId, string $role): int { + $user = $this->users->getAccountFromMail($mail); + if($user == null) { + return -1; + } + if(!$this->members->isMemberOfTeam($teamId, $user->getUser()->getId())) { + $this->members->insert($teamId, $user->getUser()->getId(), $role); + return 1; + } + return -2; } /** * @param string $name + * @param int $id * @return TeamInfo[] */ - public function listByName(string $name): array { - return $this->teams->listByName($name); + public function listByName(string $name, int $id): array { + return $this->teams->listByName($name, $id); } /** - * @param int $id - * @return Team + * @param int $idTeam + * @param int $idCurrentUser + * @return Team|null */ - public function getTeam(int $id): Team { - $teamInfo = $this->teams->getTeamById($id); - $members = $this->members->getMembersOfTeam($id); + public function getTeam(int $idTeam, int $idCurrentUser): ?Team { + if(!$this->members->isMemberOfTeam($idTeam, $idCurrentUser)) { + return null; + } + $teamInfo = $this->teams->getTeamById($idTeam); + $members = $this->members->getMembersOfTeam($idTeam); return new Team($teamInfo, $members); } - /** * delete a member from given team identifier - * @param string $mail + * @param int $idMember * @param int $teamId * @return int */ - public function deleteMember(string $mail, int $teamId): int { - $userId = $this->users->getAccountFromMail($mail)->getId(); - $this->members->remove($teamId, $userId); + public function deleteMember(int $idMember, int $teamId): int { + $this->members->remove($teamId, $idMember); + if(empty($this->members->getMembersOfTeam($teamId))) { + $this->teams->deleteTeam($teamId); + return -1; + } return $teamId; } + /** + * Delete a team + * @param string $email + * @param int $idTeam + * @return int + */ + public function deleteTeam(string $email, int $idTeam): int { + if($this->members->isCoach($email, $idTeam)) { + $this->teams->deleteTeam($idTeam); + return 0; + } + return -1; + } + + /** + * Verify if the account associated to an email is in a specific team indicated with its id + * @param int $idTeam + * @param string $email + * @return bool + */ + public function isCoach(int $idTeam, string $email): bool { + return $this->members->isCoach($email, $idTeam); + } + + /** + * Edit a team with its id, and replace the current attributes with the new ones + * @param int $idTeam + * @param string $newName + * @param string $newPicture + * @param string $newMainColor + * @param string $newSecondColor + * @return void + */ + public function editTeam(int $idTeam, string $newName, string $newPicture, string $newMainColor, string $newSecondColor) + { + $this->teams->editTeam($idTeam, $newName, $newPicture, $newMainColor, $newSecondColor); + } + /** * Get all user's teams * @@ -88,5 +140,4 @@ class TeamModel { public function getAll(int $user): array { return $this->teams->getAll($user); } - } From d6876ac0334fe15ba83acb602dcd48fb5314e0fe Mon Sep 17 00:00:00 2001 From: maxime Date: Tue, 9 Jan 2024 00:39:48 +0100 Subject: [PATCH 5/8] format and add back to home button in editor --- front/components/editor/BasketCourt.tsx | 1 - front/components/editor/SavingState.tsx | 3 ++- front/model/Team.ts | 2 +- front/style/editor.css | 2 -- front/style/new_tactic_panel.css | 1 - front/views/Editor.tsx | 6 +++++- src/App/Controller/UserController.php | 2 +- src/Core/Gateway/TeamGateway.php | 3 +-- src/Core/Model/TeamModel.php | 3 +-- 9 files changed, 11 insertions(+), 12 deletions(-) diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 1e3d916..525e232 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -9,7 +9,6 @@ import { } from "react" import CourtPlayer from "./CourtPlayer" - import { Player } from "../../model/tactic/Player" import { Action, ActionKind } from "../../model/tactic/Action" import ArrowAction from "../actions/ArrowAction" diff --git a/front/components/editor/SavingState.tsx b/front/components/editor/SavingState.tsx index ed699b4..b62358c 100644 --- a/front/components/editor/SavingState.tsx +++ b/front/components/editor/SavingState.tsx @@ -6,7 +6,8 @@ export interface SaveState { export class SaveStates { static readonly Guest: SaveState = { className: "save-state-guest", - message: "vous n'etes pas connectés, les changements seront sauvegardés sur votre navigateur.", + message: + "vous n'etes pas connectés, les changements seront sauvegardés sur votre navigateur.", } static readonly Ok: SaveState = { className: "save-state-ok", diff --git a/front/model/Team.ts b/front/model/Team.ts index 99d530b..4d0885e 100644 --- a/front/model/Team.ts +++ b/front/model/Team.ts @@ -1,4 +1,4 @@ -import {User} from "./User"; +import { User } from "./User" export interface TeamInfo { id: number diff --git a/front/style/editor.css b/front/style/editor.css index d81f38a..5ba7596 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -47,8 +47,6 @@ height: 100%; } - - #allies-rack, #opponent-rack { width: 125px; diff --git a/front/style/new_tactic_panel.css b/front/style/new_tactic_panel.css index b1043d8..f3cabeb 100644 --- a/front/style/new_tactic_panel.css +++ b/front/style/new_tactic_panel.css @@ -108,4 +108,3 @@ user-select: none; font-family: var(--text-main-font); } - diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index e1f0edd..cee7b98 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -32,6 +32,7 @@ import { CourtAction } from "./editor/CourtAction" import { BasketCourt } from "../components/editor/BasketCourt" import { ratioWithinBase } from "../components/arrows/Pos" import { Action, ActionKind } from "../model/tactic/Action" +import {BASE} from "../Constants"; const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -394,8 +395,11 @@ function EditorView({ return (
    +
    - +
    $name, ]); } - + /** * @return ViewHttpResponse account settings page */ diff --git a/src/Core/Gateway/TeamGateway.php b/src/Core/Gateway/TeamGateway.php index fe0b2bb..02b1746 100644 --- a/src/Core/Gateway/TeamGateway.php +++ b/src/Core/Gateway/TeamGateway.php @@ -105,8 +105,7 @@ class TeamGateway { * @param string $newSecondColor * @return void */ - public function editTeam(int $idTeam, string $newName, string $newPicture, string $newMainColor, string $newSecondColor) - { + public function editTeam(int $idTeam, string $newName, string $newPicture, string $newMainColor, string $newSecondColor) { $this->con->exec( "UPDATE team SET name = :newName, diff --git a/src/Core/Model/TeamModel.php b/src/Core/Model/TeamModel.php index d6c97ce..2bfe36e 100644 --- a/src/Core/Model/TeamModel.php +++ b/src/Core/Model/TeamModel.php @@ -126,8 +126,7 @@ class TeamModel { * @param string $newSecondColor * @return void */ - public function editTeam(int $idTeam, string $newName, string $newPicture, string $newMainColor, string $newSecondColor) - { + public function editTeam(int $idTeam, string $newName, string $newPicture, string $newMainColor, string $newSecondColor) { $this->teams->editTeam($idTeam, $newName, $newPicture, $newMainColor, $newSecondColor); } From ce3570e5157be0f3a65424d17f18c67dce56cdd0 Mon Sep 17 00:00:00 2001 From: maxime Date: Tue, 9 Jan 2024 00:44:43 +0100 Subject: [PATCH 6/8] fix bases --- front/views/Home.tsx | 22 +++++++--------------- front/views/template/Header.tsx | 4 +++- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/front/views/Home.tsx b/front/views/Home.tsx index 8be885d..cc758ca 100644 --- a/front/views/Home.tsx +++ b/front/views/Home.tsx @@ -1,8 +1,8 @@ import "../style/home/home.css" // import AccountSvg from "../assets/account.svg?react" -import { CSSProperties, useRef } from "react" -import { Header } from "./template/Header" +import {Header} from "./template/Header" +import {BASE} from "../Constants"; interface Tactic { id: number @@ -138,7 +138,7 @@ function TableData({ allTactics }: { allTactics: Tactic[] }) { key={tactic.id} className="data" onClick={() => { - location.pathname = "/tactic/" + tactic.id + "/edit" + location.pathname = BASE + "/tactic/" + tactic.id + "/edit" }}> {truncateString(tactic.name, 25)} @@ -177,21 +177,13 @@ function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) { } function Team({ teams }: { teams: Team[] }) { - const listTeam = teams.map((team, rowIndex) => ( -
  • - {team.name} - -
  • - )) return (

    Mes équipes

    @@ -208,7 +200,7 @@ function Tactic({ lastTactics }: { lastTactics: Tactic[] }) {
    @@ -237,7 +229,7 @@ function ButtonTeam({ team }: { team: Team }) { id={"button-team" + team.id} className="button-side-menu data" onClick={() => { - location.pathname = "/team/" + team.id + location.pathname = BASE + "/team/" + team.id }}> {name}
    @@ -252,7 +244,7 @@ function ButtonLastTactic({ tactic }: { tactic: Tactic }) { id={"button" + tactic.id} className="button-side-menu data" onClick={() => { - location.pathname = "/tactic/" + tactic.id + "/edit" + location.pathname = BASE + "/tactic/" + tactic.id + "/edit" }}> {name}
    diff --git a/front/views/template/Header.tsx b/front/views/template/Header.tsx index 7129153..3c8c6e6 100644 --- a/front/views/template/Header.tsx +++ b/front/views/template/Header.tsx @@ -1,3 +1,5 @@ +import {BASE} from "../../Constants"; + /** * * @param param0 username @@ -25,7 +27,7 @@ export function Header({ username }: { username: string }) { id="img-account" src="account.svg" onClick={() => { - location.pathname = "/settings" + location.pathname = BASE + "/settings" }} />

    {username}

    From 0aa7ad6ba2d8dc5e757a30d4deb92f9a970dee58 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Tue, 9 Jan 2024 08:40:24 +0100 Subject: [PATCH 7/8] fixes --- front/assets/court/court.svg | 63 --------------------------- src/App/Controller/AuthController.php | 12 ++++- src/Core/Http/HttpResponse.php | 13 ++++++ 3 files changed, 23 insertions(+), 65 deletions(-) delete mode 100644 front/assets/court/court.svg diff --git a/front/assets/court/court.svg b/front/assets/court/court.svg deleted file mode 100644 index e01fd58..0000000 --- a/front/assets/court/court.svg +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/App/Controller/AuthController.php b/src/App/Controller/AuthController.php index cd89d11..7df241d 100644 --- a/src/App/Controller/AuthController.php +++ b/src/App/Controller/AuthController.php @@ -48,7 +48,11 @@ class AuthController { $session->setAccount($account); $target_url = $session->getInitialTarget(); - return HttpResponse::redirect($target_url ?? "/home"); + if ($target_url != null) { + return HttpResponse::redirect_absolute($target_url); + } + + return HttpResponse::redirect("/home"); } @@ -73,7 +77,11 @@ class AuthController { $target_url = $session->getInitialTarget(); $session->setInitialTarget(null); - return HttpResponse::redirect($target_url ?? "/home"); + if ($target_url != null) { + return HttpResponse::redirect_absolute($target_url); + } + + return HttpResponse::redirect("/home"); } } diff --git a/src/Core/Http/HttpResponse.php b/src/Core/Http/HttpResponse.php index eb52ccc..c98a261 100644 --- a/src/Core/Http/HttpResponse.php +++ b/src/Core/Http/HttpResponse.php @@ -42,11 +42,24 @@ class HttpResponse { * @param int $code only HTTP 3XX codes are accepted. * @return HttpResponse a response that will redirect client to given url */ + public static function redirect(string $url, int $code = HttpCodes::FOUND): HttpResponse { + global $basePath; + return self::redirect_absolute($basePath . $url, $code); + } + + /** + * @param string $url the url to redirect + * @param int $code only HTTP 3XX codes are accepted. + * @return HttpResponse a response that will redirect client to given url + */ + + public static function redirect_absolute(string $url, int $code = HttpCodes::FOUND): HttpResponse { if ($code < 300 || $code >= 400) { throw new \InvalidArgumentException("given code is not a redirection http code"); } return new HttpResponse($code, ["Location" => $url]); } + } From fc25ddff4cb05759cd63d714122b2f6c85a3e0a6 Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Tue, 9 Jan 2024 10:41:08 +0100 Subject: [PATCH 8/8] fixed the bugs that get all the team instead of the users ones + format --- front/views/Editor.tsx | 4 ++-- front/views/Home.tsx | 4 ++-- front/views/template/Header.tsx | 2 +- src/Core/Gateway/TeamGateway.php | 14 ++++++++++---- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index cee7b98..cbb2da5 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -32,7 +32,7 @@ import { CourtAction } from "./editor/CourtAction" import { BasketCourt } from "../components/editor/BasketCourt" import { ratioWithinBase } from "../components/arrows/Pos" import { Action, ActionKind } from "../model/tactic/Action" -import {BASE} from "../Constants"; +import { BASE } from "../Constants" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -399,7 +399,7 @@ function EditorView({ Home
    - +
    > + * @param int $user + * @return array */ public function getAll(int $user): array { - return $this->con->fetch("SELECT * FROM Team", []); + return $this->con->fetch( + "SELECT t.* FROM team t,Member m WHERE m.id_team = t.id AND m.id_user= :idUser ", + [ + "idUser" => [$user, PDO::PARAM_INT], + ] + ); } + }