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 01ae8c4..fdf02a4 100644 --- a/config.php +++ b/config.php @@ -10,6 +10,8 @@ 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/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/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 diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index e59bdb7..525e232 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -9,12 +9,12 @@ 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 +255,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/components/editor/SavingState.tsx b/front/components/editor/SavingState.tsx index 68c2285..b62358c 100644 --- a/front/components/editor/SavingState.tsx +++ b/front/components/editor/SavingState.tsx @@ -6,19 +6,20 @@ 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/model/Team.ts b/front/model/Team.ts new file mode 100644 index 0000000..4d0885e --- /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/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/editor.css b/front/style/editor.css index 258476a..5ba7596 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -95,8 +95,7 @@ #court-image-div { position: relative; background-color: white; - height: 100%; - width: 100%; + height: 80vh; } .court-container { 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/new_tactic_panel.css b/front/style/new_tactic_panel.css index ff6a07e..f3cabeb 100644 --- a/front/style/new_tactic_panel.css +++ b/front/style/new_tactic_panel.css @@ -104,19 +104,7 @@ 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/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/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/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..cbb2da5 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -16,22 +16,23 @@ 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" +import { BASE } from "../Constants" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -58,7 +59,7 @@ export interface EditorProps { * information about a player that is into a rack */ interface RackedPlayer { - team: Team + team: PlayerTeam key: string } @@ -134,10 +135,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 +223,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 +358,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) { @@ -394,6 +395,9 @@ function EditorView({ return (
+
@@ -539,7 +543,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 +558,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 d44571d..a9a4b0a 100644 --- a/front/views/Home.tsx +++ b/front/views/Home.tsx @@ -1,8 +1,8 @@ -import "../style/home.css" -import "../style/personnal_space.css" -import "../style/side_menu.css" +import "../style/home/home.css" + // import AccountSvg from "../assets/account.svg?react" -import { CSSProperties, useRef } from "react" +import { Header } from "./template/Header" +import { BASE } from "../Constants" interface Tactic { id: number @@ -31,7 +31,7 @@ export default function Home({ }) { return (
- + <Header username={username} /> <Body lastTactics={lastTactics} allTactics={allTactics} @@ -41,44 +41,7 @@ export default function Home({ ) } -/** - * - * @param param0 username - * @returns Header - */ -export function Title({ username }: { username: string }) { - return ( - <div id="header"> - <div id="header-left"></div> - <div id="header-center"> - <h1 - id="IQBall" - className="clickable" - onClick={() => { - location.pathname = "/" - }}> - <span id="IQ">IQ</span> - <span id="Ball">Ball</span> - </h1> - </div> - <div id="header-right"> - <div className="clickable" id="clickable-header-right"> - {/* <AccountSvg id="img-account" /> */} - <img - id="img-account" - src="account.svg" - onClick={() => { - location.pathname = "/settings" - }} - /> - <p id="username">{username}</p> - </div> - </div> - </div> - ) -} - -export function Body({ +function Body({ lastTactics, allTactics, teams, @@ -101,7 +64,7 @@ export function Body({ ) } -export function SideMenu({ +function SideMenu({ width, lastTactics, teams, @@ -112,11 +75,11 @@ export function SideMenu({ }) { return ( <div - id="sideMenu" + id="side-menu" style={{ width: width + "%", }}> - <div id="sideMenuContent"> + <div id="side-menu-content"> <Team teams={teams} /> <Tactic lastTactics={lastTactics} /> </div> @@ -124,7 +87,7 @@ export function SideMenu({ ) } -export function PersonalSpace({ +function PersonalSpace({ width, allTactics, }: { @@ -145,7 +108,7 @@ export function PersonalSpace({ function TitlePersonalSpace() { return ( - <div id="titlePersonalSpace"> + <div id="title-personal-space"> <h2>Espace Personnel</h2> </div> ) @@ -175,9 +138,9 @@ function TableData({ allTactics }: { allTactics: Tactic[] }) { key={tactic.id} className="data" onClick={() => { - location.pathname = "/tactic/" + tactic.id + "/edit" + location.pathname = BASE + "/tactic/" + tactic.id + "/edit" }}> - {troncName(tactic.name, 25)} + {truncateString(tactic.name, 25)} </td> )) i++ @@ -199,13 +162,13 @@ function TableData({ allTactics }: { allTactics: Tactic[] }) { function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) { let data if (allTactics.length == 0) { - data = <p>Aucune tactique créé !</p> + data = <p>Aucune tactique créée !</p> } else { data = <TableData allTactics={allTactics} /> } return ( - <div id="bodyPersonalSpace"> + <div id="body-personal-space"> <table> <tbody key="tbody">{data}</tbody> </table> @@ -213,22 +176,14 @@ function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) { ) } -export function Team({ teams }: { teams: Team[] }) { - const listTeam = teams.map((team, rowIndex) => ( - <li key={"team" + rowIndex}> - {team.name} - <button onClick={() => (location.pathname = "/team/" + team.id)}> - open - </button> - </li> - )) +function Team({ teams }: { teams: Team[] }) { return ( <div id="teams"> - <div className="titreSideMenu"> + <div className="titre-side-menu"> <h2 className="title">Mes équipes</h2> <button className="new" - onClick={() => (location.pathname = "/team/new")}> + onClick={() => (location.pathname = BASE + "/team/new")}> + </button> </div> @@ -237,15 +192,15 @@ export function Team({ teams }: { teams: Team[] }) { ) } -export function Tactic({ lastTactics }: { lastTactics: Tactic[] }) { +function Tactic({ lastTactics }: { lastTactics: Tactic[] }) { return ( <div id="tactic"> - <div className="titreSideMenu"> + <div className="titre-side-menu"> <h2 className="title">Mes dernières stratégies</h2> <button className="new" - id="createTactic" - onClick={() => (location.pathname = "/tactic/new")}> + id="create-tactic" + onClick={() => (location.pathname = BASE + "/tactic/new")}> + </button> </div> @@ -258,23 +213,23 @@ function SetButtonTactic({ tactics }: { tactics: Tactic[] }) { const lastTactics = tactics.map((tactic) => ( <ButtonLastTactic tactic={tactic} /> )) - return <div className="SetButton">{lastTactics}</div> + return <div className="set-button">{lastTactics}</div> } function SetButtonTeam({ teams }: { teams: Team[] }) { const listTeam = teams.map((teams) => <ButtonTeam team={teams} />) - return <div className="SetButton">{listTeam}</div> + return <div className="set-button">{listTeam}</div> } function ButtonTeam({ team }: { team: Team }) { - const name = troncName(team.name, 20) + const name = truncateString(team.name, 20) return ( <div> <div - id={"ButtonTeam" + team.id} - className="buttonSideMenu data" + id={"button-team" + team.id} + className="button-side-menu data" onClick={() => { - location.pathname = "/team/" + team.id + location.pathname = BASE + "/team/" + team.id }}> {name} </div> @@ -283,24 +238,22 @@ function ButtonTeam({ team }: { team: Team }) { } function ButtonLastTactic({ tactic }: { tactic: Tactic }) { - const name = troncName(tactic.name, 20) + const name = truncateString(tactic.name, 20) return ( <div - id={"Button" + tactic.id} - className="buttonSideMenu data" + id={"button" + tactic.id} + className="button-side-menu data" onClick={() => { - location.pathname = "/tactic/" + tactic.id + "/edit" + location.pathname = BASE + "/tactic/" + tactic.id + "/edit" }}> {name} </div> ) } -function troncName(name: string, limit: number): string { +function truncateString(name: string, limit: number): string { if (name.length > limit) { name = name.substring(0, limit) + "..." - } else { - name = name } return name } 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 ( <div id={"panel-root"}> <div id={"panel-top"}> - <p>Select a basket court</p> + <p>Selectionnez un terrain</p> </div> <div id={"panel-choices"}> <div id={"panel-buttons"}> <CourtKindButton - name="Plain" - details="Select a plain basketball court" + name="Terrain complet" image={plainCourt} redirect="/tactic/new/plain" /> <CourtKindButton - name="Half" - details="Select half a basketball court" + name="Demi-terrain" image={halfCourt} redirect="/tactic/new/half" /> @@ -34,19 +32,16 @@ export default function NewTacticPanel() { function CourtKindButton({ name, image, - details, redirect, }: { name: string image: string - details: string redirect: string }) { return ( <div className="court-kind-button" onClick={() => (location.href = BASE + redirect)}> - <div className="court-kind-button-details">{details}</div> <div className="court-kind-button-top"> <div className="court-kind-button-image-div"> <img 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 ( + <div id="main-div"> + <header> + <h1> + <a href={BASE + "/"}>IQBall</a> + </h1> + </header> + <TeamDisplay team={team.info} /> + + {isCoach && <CoachOptions id={team.info.id} />} + + <MembersDisplay + members={team.members} + isCoach={isCoach} + idTeam={team.info.id} + currentUserId={currentUserId} + /> + </div> + ) +} + +function TeamDisplay({ team }: { team: TeamInfo }) { + return ( + <div id="team-info"> + <div id="first-part"> + <h1 id="team-name">{team.name}</h1> + <img id="logo" src={team.picture} alt="Logo d'équipe" /> + </div> + <div id="colors"> + <div id="colorsTitle"> + <p>Couleur principale</p> + <p>Couleur secondaire</p> + </div> + <div id="actual-colors"> + <ColorDisplay color={team.mainColor} /> + <ColorDisplay color={team.secondColor} /> + </div> + </div> + </div> + ) +} + +function ColorDisplay({ color }: { color: string }) { + return <div className="square" style={{ backgroundColor: color }} /> +} + +function CoachOptions({ id }: { id: number }) { + return ( + <div> + <button + id="delete" + onClick={() => + confirm("Êtes-vous sûr de supprimer cette équipe?") + ? (window.location.href = `${BASE}/team/${id}/delete`) + : {} + }> + Supprimer + </button> + <button + id="edit" + onClick={() => + (window.location.href = `${BASE}/team/${id}/edit`) + }> + Modifier + </button> + </div> + ) +} + +function MembersDisplay({ + members, + isCoach, + idTeam, + currentUserId, +}: { + members: Member[] + isCoach: boolean + idTeam: number + currentUserId: number +}) { + const listMember = members.map((member) => ( + <MemberDisplay + member={member} + isCoach={isCoach} + idTeam={idTeam} + currentUserId={currentUserId} + /> + )) + return ( + <div id="members"> + <div id="head-members"> + <h2>Membres :</h2> + {isCoach && ( + <button + id="add-member" + onClick={() => + (window.location.href = `${BASE}/team/${idTeam}/addMember`) + }> + + + </button> + )} + </div> + {listMember} + </div> + ) +} + +function MemberDisplay({ + member, + isCoach, + idTeam, + currentUserId, +}: { + member: Member + isCoach: boolean + idTeam: number + currentUserId: number +}) { + return ( + <div className="member"> + <img + id="profile-picture" + src={member.user.profilePicture} + alt="Photo de profile" + /> + <p>{member.user.name}</p> + <p>{member.role}</p> + <p>{member.user.email}</p> + {isCoach && currentUserId !== member.user.id && ( + <button + id="delete" + onClick={() => + confirm( + "Êtes-vous sûr de retirer ce membre de l'équipe?", + ) + ? (window.location.href = `${BASE}/team/${idTeam}/remove/${member.user.id}`) + : {} + }> + Retirer + </button> + )} + {isCoach && currentUserId == member.user.id && ( + <button + id="delete" + onClick={() => + confirm("Êtes-vous sûr de quitter cette équipe?") + ? (window.location.href = `${BASE}/team/${idTeam}/remove/${member.user.id}`) + : {} + }> + Quitter + </button> + )} + </div> + ) +} 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/front/views/template/Header.tsx b/front/views/template/Header.tsx new file mode 100644 index 0000000..2618d90 --- /dev/null +++ b/front/views/template/Header.tsx @@ -0,0 +1,38 @@ +import { BASE } from "../../Constants" + +/** + * + * @param param0 username + * @returns Header + */ +export function Header({ username }: { username: string }) { + return ( + <div id="header"> + <div id="header-left"></div> + <div id="header-center"> + <h1 + id="iqball" + className="clickable" + onClick={() => { + location.pathname = "/" + }}> + <span id="IQ">IQ</span> + <span id="Ball">Ball</span> + </h1> + </div> + <div id="header-right"> + <div className="clickable" id="clickable-header-right"> + {/* <AccountSvg id="img-account" /> */} + <img + id="img-account" + src="account.svg" + onClick={() => { + location.pathname = BASE + "/settings" + }} + /> + <p id="username">{username}</p> + </div> + </div> + </div> + ) +} 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/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/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<string, mixed> $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<string, mixed> $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<string,mixed> $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 a995879..6f56128 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,13 +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(); - //TODO + $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,18 +48,13 @@ class UserController { "teams" => $teams, "username" => $name, ]); - // return ViewHttpResponse::react("views/Home.tsx", []); - } - - 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/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; + } </style> </head> <body> +<header> + <h1><a href="{{ path('/') }}">IQBall</a></h1> +</header> <div class="container"> <h2>Ajouter un membre à votre équipe</h2> - <form action="{{ path('/team/members/add') }}" method="POST"> + <form action="{{ path("/team/#{idTeam}/addMember") }}" method="POST"> <div class="form-group"> - <label for="team">Team où ajouter le membre :</label> - <input type="text" id="team" name="team" required> - <label for="mail">Email du membre :</label> - <input type="text" id="mail" name="mail" required> + + <label for="email">Email du membre :</label> + {% if badEmail %} + <p class="failed">Email invalide</p> + {% endif %} + {%if notFound %} + <p class="failed">Cette personne n'a pas été trouvé</p> + {% endif %} + {% if alreadyExisting %} + <p class="failed">Cette personne est déjà dans l'équipe</p> + {% endif %} + <input type="text" id="email" name="email" required> <fieldset class="role"> <legend>Rôle du membre dans l'équipe :</legend> <div class="radio"> <label for="P">Joueur</label> - <input type="radio" id="P" name="role" value="P" checked /> + <input type="radio" id="P" name="role" value="PLAYER" checked /> </div> <div class="radio"> <label for="C">Coach</label> - <input type="radio" id="C" name="role" value="C" /> + <input type="radio" id="C" name="role" value="COACH" /> </div> </fieldset> 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; + } </style> </head> <body> @@ -61,13 +65,18 @@ </header> <section class="container"> - - <div class="team container"> + {% if notDeleted %} + <popup> + <p>Cette équipe ne peut être supprimée.</p> + </popup> + {% endif %} +{% if team is defined %} + <div class="team"> <div> <h1>{{ team.getInfo().getName() }}</h1> <img src="{{ team.getInfo().getPicture() }}" alt="Logo d'équipe" class="logo"> </div> - <div> + <div id="colors"> <div class="color"><p>Couleur principale : </p> <div class="square" id="main_color"></div> </div> @@ -75,17 +84,26 @@ <div class="square" id="second_color"></div> </div> </div> - + {% if isCoach %} + <button id="delete" onclick="confirm('Êtes-vous sûr de supprimer cette équipe?') ? window.location.href = '{{ path("/team/#{team.getInfo().getId()}/delete") }}' : {}">Supprimer</button> + <button></button> + {% endif %} {% for m in team.listMembers() %} - <p> {{ m.getUserId() }} </p> - {% if m.getRole().isCoach() %} + <div class="player"> + <p> {{ m.getUserId() }} </p> + {% if m.getRole().isCoach() %} <p> : Coach</p> - {% else %} + {% else %} <p> : Joueur</p> - {% endif %} + {% endif %} + </div> {% endfor %} </div> - +{% else %} + <div> + <h3>Cette équipe ne peut être affichée</h3> + </div> +{% endif %} </section> </body> </html> \ 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 @@ <head> <meta charset="UTF-8"> <title>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 c5cc115..a817687 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,21 +72,70 @@ 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; } /** - * Undocumented function - * - * @param integer $user - * @return array> + * @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], + ] + ); + + } + + /** + * @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], + ] + ); } - + + } 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]); } + } 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 3d5ade4..2bfe36e 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,56 +37,106 @@ 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 * * @param integer $user * @return array> */ - public function getAll(int $user) : array { + public function getAll(int $user): array { return $this->teams->getAll($user); } - }