Merge branch 'master' of codefirst.iut.uca.fr:IQBall/Application-Web

DahmaneYanis 1 year ago
commit ea39aca556

1
.gitignore vendored

@ -1,4 +1,5 @@
.vs .vs
.vscode
.idea .idea
.code .code
.vite .vite

@ -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. * 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 * @param string $assetURI relative uri path from `/front` folder
* @return string valid url that points to the given uri
*/ */
function asset(string $assetURI): string { function asset(string $assetURI): string {
return _asset($assetURI); return _asset($assetURI);

@ -1,5 +1,4 @@
<svg width="269" height="309" viewBox="0 0 269 309" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="269" height="309" viewBox="0 0 269 309" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="254" width="250" height="269" transform="rotate(-90 0 254)" fill="#D9D9D9"/>
<line x1="24" y1="236" x2="24" y2="26" stroke="black" stroke-width="2"/> <line x1="24" y1="236" x2="24" y2="26" stroke="black" stroke-width="2"/>
<line x1="248" y1="25" x2="248" y2="236" stroke="black" stroke-width="2"/> <line x1="248" y1="25" x2="248" y2="236" stroke="black" stroke-width="2"/>
<line x1="249" y1="237" x2="23" y2="237" stroke="black" stroke-width="2"/> <line x1="249" y1="237" x2="23" y2="237" stroke="black" stroke-width="2"/>

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

@ -116,7 +116,8 @@ export default function BendableArrow({
const styleWidth = style?.width ?? ArrowStyleDefaults.width const styleWidth = style?.width ?? ArrowStyleDefaults.width
const computeInternalSegments = useCallback((segments: Segment[]) => { const computeInternalSegments = useCallback(
(segments: Segment[]) => {
return segments.map((segment, idx) => { return segments.map((segment, idx) => {
if (idx == 0) { if (idx == 0) {
return { return {
@ -132,7 +133,9 @@ export default function BendableArrow({
end: segment.next, end: segment.next,
} }
}) })
}, [segments, startPos]) },
[segments, startPos],
)
// Cache the segments so that when the user is changing the segments (it moves an ArrowPoint), // 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. // it does not unwind to this arrow's component parent until validated.
@ -151,8 +154,6 @@ export default function BendableArrow({
const headRef = useRef<HTMLDivElement>(null) const headRef = useRef<HTMLDivElement>(null)
const tailRef = useRef<HTMLDivElement>(null) const tailRef = useRef<HTMLDivElement>(null)
/** /**
* Computes and return the segments edition points * Computes and return the segments edition points
* @param parentBase * @param parentBase

@ -9,12 +9,12 @@ import {
} from "react" } from "react"
import CourtPlayer from "./CourtPlayer" import CourtPlayer from "./CourtPlayer"
import { Player } from "../../tactic/Player" import { Player } from "../../model/tactic/Player"
import { Action, ActionKind } from "../../tactic/Action" import { Action, ActionKind } from "../../model/tactic/Action"
import ArrowAction from "../actions/ArrowAction" import ArrowAction from "../actions/ArrowAction"
import { middlePos, ratioWithinBase } from "../arrows/Pos" import { middlePos, ratioWithinBase } from "../arrows/Pos"
import BallAction from "../actions/BallAction" import BallAction from "../actions/BallAction"
import { CourtObject } from "../../tactic/CourtObjects" import { CourtObject } from "../../model/tactic/Ball"
import { contains } from "../arrows/Box" import { contains } from "../arrows/Box"
import { CourtAction } from "../../views/editor/CourtAction" 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 && ( {previewAction && (

@ -1,7 +1,7 @@
import React, { useRef } from "react" import React, { useRef } from "react"
import Draggable from "react-draggable" import Draggable from "react-draggable"
import { BallPiece } from "./BallPiece" import { BallPiece } from "./BallPiece"
import { Ball } from "../../tactic/CourtObjects" import { Ball } from "../../model/tactic/Ball"
export interface CourtBallProps { export interface CourtBallProps {
onMoved: (rect: DOMRect) => void onMoved: (rect: DOMRect) => void

@ -2,7 +2,7 @@ import { ReactNode, RefObject, useRef } from "react"
import "../../style/player.css" import "../../style/player.css"
import Draggable from "react-draggable" import Draggable from "react-draggable"
import { PlayerPiece } from "./PlayerPiece" import { PlayerPiece } from "./PlayerPiece"
import { Player } from "../../tactic/Player" import { Player } from "../../model/tactic/Player"
import { NULL_POS, ratioWithinBase } from "../arrows/Pos" import { NULL_POS, ratioWithinBase } from "../arrows/Pos"
export interface PlayerProps { export interface PlayerProps {

@ -1,8 +1,8 @@
import "../../style/player.css" import "../../style/player.css"
import { Team } from "../../tactic/Team" import { PlayerTeam } from "../../model/tactic/Player"
export interface PlayerPieceProps { export interface PlayerPieceProps {
team: Team team: PlayerTeam
text: string text: string
hasBall: boolean hasBall: boolean
} }

@ -6,19 +6,20 @@ export interface SaveState {
export class SaveStates { export class SaveStates {
static readonly Guest: SaveState = { static readonly Guest: SaveState = {
className: "save-state-guest", 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 = { static readonly Ok: SaveState = {
className: "save-state-ok", className: "save-state-ok",
message: "saved", message: "sauvegardé",
} }
static readonly Saving: SaveState = { static readonly Saving: SaveState = {
className: "save-state-saving", className: "save-state-saving",
message: "saving...", message: "sauvegarde...",
} }
static readonly Err: SaveState = { static readonly Err: SaveState = {
className: "save-state-error", className: "save-state-error",
message: "could not save tactic.", message: "erreur lors de la sauvegarde.",
} }
} }

@ -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
}

@ -0,0 +1,6 @@
export interface User {
id: number
name: string
email: string
profilePicture: string
}

@ -1,5 +1,5 @@
import { Pos } from "../components/arrows/Pos" import { Pos } from "../../components/arrows/Pos"
import { Segment } from "../components/arrows/BendableArrow" import { Segment } from "../../components/arrows/BendableArrow"
import { PlayerId } from "./Player" import { PlayerId } from "./Player"
export enum ActionKind { export enum ActionKind {

@ -1,14 +1,17 @@
import { Team } from "./Team"
export type PlayerId = string export type PlayerId = string
export enum PlayerTeam {
Allies = "allies",
Opponents = "opponents",
}
export interface Player { export interface Player {
readonly id: PlayerId readonly id: PlayerId
/** /**
* the player's team * the player's team
* */ * */
readonly team: Team readonly team: PlayerTeam
/** /**
* player's role * player's role

@ -1,5 +1,5 @@
import { Player } from "./Player" import { Player } from "./Player"
import { CourtObject } from "./CourtObjects" import { CourtObject } from "./Ball"
import { Action } from "./Action" import { Action } from "./Action"
export interface Tactic { export interface Tactic {

@ -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;
}

@ -95,8 +95,7 @@
#court-image-div { #court-image-div {
position: relative; position: relative;
background-color: white; background-color: white;
height: 100%; height: 80vh;
width: 100%;
} }
.court-container { .court-container {

@ -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%;
}

@ -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;
}

@ -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;
}

@ -104,19 +104,7 @@
background-color: var(--second-color); background-color: var(--second-color);
} }
.court-kind-button-name, .court-kind-button-name {
.court-kind-button-details {
user-select: none; user-select: none;
font-family: var(--text-main-font); 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;
}

@ -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;
}

@ -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;
}

@ -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;
}

@ -1,4 +0,0 @@
export enum Team {
Allies = "allies",
Opponents = "opponents",
}

@ -16,22 +16,23 @@ import { BallPiece } from "../components/editor/BallPiece"
import { Rack } from "../components/Rack" import { Rack } from "../components/Rack"
import { PlayerPiece } from "../components/editor/PlayerPiece" 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 { fetchAPI } from "../Fetcher"
import { Team } from "../tactic/Team" import { PlayerTeam } from "../model/tactic/Player"
import SavingState, { import SavingState, {
SaveState, SaveState,
SaveStates, SaveStates,
} from "../components/editor/SavingState" } from "../components/editor/SavingState"
import { CourtObject } from "../tactic/CourtObjects" import { CourtObject } from "../model/tactic/Ball"
import { CourtAction } from "./editor/CourtAction" import { CourtAction } from "./editor/CourtAction"
import { BasketCourt } from "../components/editor/BasketCourt" import { BasketCourt } from "../components/editor/BasketCourt"
import { ratioWithinBase } from "../components/arrows/Pos" 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 = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
@ -58,7 +59,7 @@ export interface EditorProps {
* information about a player that is into a rack * information about a player that is into a rack
*/ */
interface RackedPlayer { interface RackedPlayer {
team: Team team: PlayerTeam
key: string key: string
} }
@ -134,10 +135,10 @@ function EditorView({
) )
const [allies, setAllies] = useState( const [allies, setAllies] = useState(
getRackPlayers(Team.Allies, content.players), getRackPlayers(PlayerTeam.Allies, content.players),
) )
const [opponents, setOpponents] = useState( const [opponents, setOpponents] = useState(
getRackPlayers(Team.Opponents, content.players), getRackPlayers(PlayerTeam.Opponents, content.players),
) )
const [objects, setObjects] = useState<RackedCourtObject[]>( const [objects, setObjects] = useState<RackedCourtObject[]>(
@ -222,7 +223,7 @@ function EditorView({
break break
default: default:
throw new Error("unknown court object ", rackedObject.key) throw new Error("unknown court object " + rackedObject.key)
} }
setContent((content) => { setContent((content) => {
@ -357,10 +358,10 @@ function EditorView({
})) }))
let setter let setter
switch (player.team) { switch (player.team) {
case Team.Opponents: case PlayerTeam.Opponents:
setter = setOpponents setter = setOpponents
break break
case Team.Allies: case PlayerTeam.Allies:
setter = setAllies setter = setAllies
} }
if (player.hasBall) { if (player.hasBall) {
@ -394,6 +395,9 @@ function EditorView({
return ( return (
<div id="main-div"> <div id="main-div">
<div id="topbar-div"> <div id="topbar-div">
<button onClick={() => (location.pathname = BASE + "/")}>
Home
</button>
<div id="topbar-left"> <div id="topbar-left">
<SavingState state={saveState} /> <SavingState state={saveState} />
</div> </div>
@ -539,7 +543,7 @@ function renderCourtObject(courtObject: RackedCourtObject) {
if (courtObject.key == "ball") { if (courtObject.key == "ball") {
return <BallPiece /> return <BallPiece />
} }
throw new Error("unknown racked court object ", courtObject.key) throw new Error("unknown racked court object " + courtObject.key)
} }
function Court({ courtType }: { courtType: string }) { 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"] return ["1", "2", "3", "4", "5"]
.filter( .filter(
(role) => (role) =>

@ -1,8 +1,8 @@
import "../style/home.css" import "../style/home/home.css"
import "../style/personnal_space.css"
import "../style/side_menu.css"
// import AccountSvg from "../assets/account.svg?react" // import AccountSvg from "../assets/account.svg?react"
import { CSSProperties, useRef } from "react" import { Header } from "./template/Header"
import { BASE } from "../Constants"
interface Tactic { interface Tactic {
id: number id: number
@ -31,7 +31,7 @@ export default function Home({
}) { }) {
return ( return (
<div id="main"> <div id="main">
<Title username={username} /> <Header username={username} />
<Body <Body
lastTactics={lastTactics} lastTactics={lastTactics}
allTactics={allTactics} allTactics={allTactics}
@ -41,44 +41,7 @@ export default function Home({
) )
} }
/** function Body({
*
* @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({
lastTactics, lastTactics,
allTactics, allTactics,
teams, teams,
@ -101,7 +64,7 @@ export function Body({
) )
} }
export function SideMenu({ function SideMenu({
width, width,
lastTactics, lastTactics,
teams, teams,
@ -112,11 +75,11 @@ export function SideMenu({
}) { }) {
return ( return (
<div <div
id="sideMenu" id="side-menu"
style={{ style={{
width: width + "%", width: width + "%",
}}> }}>
<div id="sideMenuContent"> <div id="side-menu-content">
<Team teams={teams} /> <Team teams={teams} />
<Tactic lastTactics={lastTactics} /> <Tactic lastTactics={lastTactics} />
</div> </div>
@ -124,7 +87,7 @@ export function SideMenu({
) )
} }
export function PersonalSpace({ function PersonalSpace({
width, width,
allTactics, allTactics,
}: { }: {
@ -145,7 +108,7 @@ export function PersonalSpace({
function TitlePersonalSpace() { function TitlePersonalSpace() {
return ( return (
<div id="titlePersonalSpace"> <div id="title-personal-space">
<h2>Espace Personnel</h2> <h2>Espace Personnel</h2>
</div> </div>
) )
@ -175,9 +138,9 @@ function TableData({ allTactics }: { allTactics: Tactic[] }) {
key={tactic.id} key={tactic.id}
className="data" className="data"
onClick={() => { onClick={() => {
location.pathname = "/tactic/" + tactic.id + "/edit" location.pathname = BASE + "/tactic/" + tactic.id + "/edit"
}}> }}>
{troncName(tactic.name, 25)} {truncateString(tactic.name, 25)}
</td> </td>
)) ))
i++ i++
@ -199,13 +162,13 @@ function TableData({ allTactics }: { allTactics: Tactic[] }) {
function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) { function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) {
let data let data
if (allTactics.length == 0) { if (allTactics.length == 0) {
data = <p>Aucune tactique créé !</p> data = <p>Aucune tactique créée !</p>
} else { } else {
data = <TableData allTactics={allTactics} /> data = <TableData allTactics={allTactics} />
} }
return ( return (
<div id="bodyPersonalSpace"> <div id="body-personal-space">
<table> <table>
<tbody key="tbody">{data}</tbody> <tbody key="tbody">{data}</tbody>
</table> </table>
@ -213,22 +176,14 @@ function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) {
) )
} }
export function Team({ teams }: { teams: Team[] }) { 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>
))
return ( return (
<div id="teams"> <div id="teams">
<div className="titreSideMenu"> <div className="titre-side-menu">
<h2 className="title">Mes équipes</h2> <h2 className="title">Mes équipes</h2>
<button <button
className="new" className="new"
onClick={() => (location.pathname = "/team/new")}> onClick={() => (location.pathname = BASE + "/team/new")}>
+ +
</button> </button>
</div> </div>
@ -237,15 +192,15 @@ export function Team({ teams }: { teams: Team[] }) {
) )
} }
export function Tactic({ lastTactics }: { lastTactics: Tactic[] }) { function Tactic({ lastTactics }: { lastTactics: Tactic[] }) {
return ( return (
<div id="tactic"> <div id="tactic">
<div className="titreSideMenu"> <div className="titre-side-menu">
<h2 className="title">Mes dernières stratégies</h2> <h2 className="title">Mes dernières stratégies</h2>
<button <button
className="new" className="new"
id="createTactic" id="create-tactic"
onClick={() => (location.pathname = "/tactic/new")}> onClick={() => (location.pathname = BASE + "/tactic/new")}>
+ +
</button> </button>
</div> </div>
@ -258,23 +213,23 @@ function SetButtonTactic({ tactics }: { tactics: Tactic[] }) {
const lastTactics = tactics.map((tactic) => ( const lastTactics = tactics.map((tactic) => (
<ButtonLastTactic tactic={tactic} /> <ButtonLastTactic tactic={tactic} />
)) ))
return <div className="SetButton">{lastTactics}</div> return <div className="set-button">{lastTactics}</div>
} }
function SetButtonTeam({ teams }: { teams: Team[] }) { function SetButtonTeam({ teams }: { teams: Team[] }) {
const listTeam = teams.map((teams) => <ButtonTeam team={teams} />) 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 }) { function ButtonTeam({ team }: { team: Team }) {
const name = troncName(team.name, 20) const name = truncateString(team.name, 20)
return ( return (
<div> <div>
<div <div
id={"ButtonTeam" + team.id} id={"button-team" + team.id}
className="buttonSideMenu data" className="button-side-menu data"
onClick={() => { onClick={() => {
location.pathname = "/team/" + team.id location.pathname = BASE + "/team/" + team.id
}}> }}>
{name} {name}
</div> </div>
@ -283,24 +238,22 @@ function ButtonTeam({ team }: { team: Team }) {
} }
function ButtonLastTactic({ tactic }: { tactic: Tactic }) { function ButtonLastTactic({ tactic }: { tactic: Tactic }) {
const name = troncName(tactic.name, 20) const name = truncateString(tactic.name, 20)
return ( return (
<div <div
id={"Button" + tactic.id} id={"button" + tactic.id}
className="buttonSideMenu data" className="button-side-menu data"
onClick={() => { onClick={() => {
location.pathname = "/tactic/" + tactic.id + "/edit" location.pathname = BASE + "/tactic/" + tactic.id + "/edit"
}}> }}>
{name} {name}
</div> </div>
) )
} }
function troncName(name: string, limit: number): string { function truncateString(name: string, limit: number): string {
if (name.length > limit) { if (name.length > limit) {
name = name.substring(0, limit) + "..." name = name.substring(0, limit) + "..."
} else {
name = name
} }
return name return name
} }

@ -9,19 +9,17 @@ export default function NewTacticPanel() {
return ( return (
<div id={"panel-root"}> <div id={"panel-root"}>
<div id={"panel-top"}> <div id={"panel-top"}>
<p>Select a basket court</p> <p>Selectionnez un terrain</p>
</div> </div>
<div id={"panel-choices"}> <div id={"panel-choices"}>
<div id={"panel-buttons"}> <div id={"panel-buttons"}>
<CourtKindButton <CourtKindButton
name="Plain" name="Terrain complet"
details="Select a plain basketball court"
image={plainCourt} image={plainCourt}
redirect="/tactic/new/plain" redirect="/tactic/new/plain"
/> />
<CourtKindButton <CourtKindButton
name="Half" name="Demi-terrain"
details="Select half a basketball court"
image={halfCourt} image={halfCourt}
redirect="/tactic/new/half" redirect="/tactic/new/half"
/> />
@ -34,19 +32,16 @@ export default function NewTacticPanel() {
function CourtKindButton({ function CourtKindButton({
name, name,
image, image,
details,
redirect, redirect,
}: { }: {
name: string name: string
image: string image: string
details: string
redirect: string redirect: string
}) { }) {
return ( return (
<div <div
className="court-kind-button" className="court-kind-button"
onClick={() => (location.href = BASE + redirect)}> onClick={() => (location.href = BASE + redirect)}>
<div className="court-kind-button-details">{details}</div>
<div className="court-kind-button-top"> <div className="court-kind-button-top">
<div className="court-kind-button-image-div"> <div className="court-kind-button-image-div">
<img <img

@ -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>
)
}

@ -1,4 +1,4 @@
import { Action, ActionKind } from "../../tactic/Action" import { Action, ActionKind } from "../../model/tactic/Action"
import BendableArrow from "../../components/arrows/BendableArrow" import BendableArrow from "../../components/arrows/BendableArrow"
import { RefObject } from "react" import { RefObject } from "react"
import { MoveToHead, ScreenHead } from "../../components/actions/ArrowAction" import { MoveToHead, ScreenHead } from "../../components/actions/ArrowAction"

@ -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>
)
}

@ -51,7 +51,6 @@ function tryGetAuthorization(): ?Account {
$session = PhpSessionHandle::init(); $session = PhpSessionHandle::init();
return $session->getAccount(); return $session->getAccount();
} }
$token = $headers['Authorization']; $token = $headers['Authorization'];
$gateway = new AccountGateway(new Connection(get_database())); $gateway = new AccountGateway(new Connection(get_database()));
return $gateway->getAccountFromToken($token); return $gateway->getAccountFromToken($token);

@ -102,10 +102,13 @@ function getRoutes(): AltoRouter {
$ar->map("GET", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->displayListTeamByName($s))); $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("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/[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("GET", "/team/[i:id]/delete", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->deleteTeamById($id, $s)));
$ar->map("POST", "/team/members/add", Action::auth(fn(SessionHandle $s) => getTeamController()->addMember($_POST, $s))); $ar->map("GET", "/team/[i:id]/addMember", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->displayAddMember($id, $s)));
$ar->map("GET", "/team/members/remove", Action::auth(fn(SessionHandle $s) => getTeamController()->displayDeleteMember($s))); $ar->map("POST", "/team/[i:id]/addMember", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->addMember($id, $_POST, $s)));
$ar->map("POST", "/team/members/remove", Action::auth(fn(SessionHandle $s) => getTeamController()->deleteMember($_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; return $ar;
} }

@ -10,7 +10,8 @@ CREATE TABLE Account
email varchar UNIQUE NOT NULL, email varchar UNIQUE NOT NULL,
username varchar NOT NULL, username varchar NOT NULL,
token varchar UNIQUE NOT NULL, token varchar UNIQUE NOT NULL,
hash varchar NOT NULL hash varchar NOT NULL,
profilePicture varchar NOT NULL
); );
CREATE TABLE Tactic CREATE TABLE Tactic

@ -36,7 +36,7 @@ class APITacticController {
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()],
], function (HttpRequest $request) use ($tactic_id, $account) { ], 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)) { if (!empty($failures)) {
//TODO find a system to handle Unauthorized error codes more easily from failures. //TODO find a system to handle Unauthorized error codes more easily from failures.

@ -48,7 +48,11 @@ class AuthController {
$session->setAccount($account); $session->setAccount($account);
$target_url = $session->getInitialTarget(); $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(); $target_url = $session->getInitialTarget();
$session->setInitialTarget(null); $session->setInitialTarget(null);
return HttpResponse::redirect($target_url ?? "/home"); if ($target_url != null) {
return HttpResponse::redirect_absolute($target_url);
}
return HttpResponse::redirect("/home");
} }
} }

@ -63,7 +63,7 @@ class EditorController {
return $this->openTestEditor($type); 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); return $this->openEditorFor($tactic);
} }
@ -76,7 +76,7 @@ class EditorController {
public function openEditor(int $id, SessionHandle $session): ViewHttpResponse { public function openEditor(int $id, SessionHandle $session): ViewHttpResponse {
$tactic = $this->model->get($id); $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) { if ($failure != null) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);

@ -4,10 +4,13 @@ namespace IQBall\App\Controller;
use IQBall\App\Session\SessionHandle; use IQBall\App\Session\SessionHandle;
use IQBall\App\ViewHttpResponse; use IQBall\App\ViewHttpResponse;
use IQBall\Core\Data\Account;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Model\TeamModel; use IQBall\Core\Model\TeamModel;
use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\FieldValidationFail;
use IQBall\Core\Validation\ValidationFail;
use IQBall\Core\Validation\Validators; use IQBall\Core\Validation\Validators;
class TeamController { class TeamController {
@ -28,16 +31,6 @@ class TeamController {
return ViewHttpResponse::twig("insert_team.html.twig", []); 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 * @param SessionHandle $session
* @return ViewHttpResponse the team panel to delete a member * @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]); return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]);
} }
$teamId = $this->model->createTeam($request['name'], $request['picture'], $request['main_color'], $request['second_color']); $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]); 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)) { if (empty($teams)) {
return ViewHttpResponse::twig('display_teams.html.twig', []); return ViewHttpResponse::twig('display_teams.html.twig', []);
} }
return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $teams]); 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 int $id
* @param SessionHandle $session * @param SessionHandle $session
* @return ViewHttpResponse a view that displays given team information * @return ViewHttpResponse a view that displays given team information
*/ */
public function displayTeam(int $id, SessionHandle $session): ViewHttpResponse { public function displayTeam(int $id, SessionHandle $session): ViewHttpResponse {
$result = $this->model->getTeam($id); $result = $this->model->getTeam($id, $session->getAccount()->getUser()->getId());
return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]); 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 * add a member to a team
* @param int $idTeam
* @param array<string, mixed> $request * @param array<string, mixed> $request
* @param SessionHandle $session * @param SessionHandle $session
* @return HttpResponse * @return HttpResponse
*/ */
public function addMember(array $request, SessionHandle $session): HttpResponse { public function addMember(int $idTeam, array $request, SessionHandle $session): HttpResponse {
$errors = []; $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, [ $request = HttpRequest::from($request, $errors, [
"team" => [Validators::isInteger()],
"email" => [Validators::email(), Validators::lenBetween(5, 256)], "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']);
$teamId = intval($request['team']); switch($ret) {
$this->model->addMember($request['email'], $teamId, $request['role']); 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);
}
}
/**
* 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); return $this->displayTeam($teamId, $session);
} }
/** /**
* remove a member from a team * @param int $idTeam
* @param SessionHandle $session
* @return ViewHttpResponse
*/
public function displayEditTeam(int $idTeam, SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("edit_team.html.twig", ['team' => $this->model->getTeam($idTeam, $session->getAccount()->getUser()->getId())]);
}
/**
* @param int $idTeam
* @param array<string,mixed> $request * @param array<string,mixed> $request
* @param SessionHandle $session * @param SessionHandle $session
* @return HttpResponse * @return HttpResponse
*/ */
public function deleteMember(array $request, SessionHandle $session): HttpResponse { public function editTeam(int $idTeam, array $request, SessionHandle $session): HttpResponse {
$errors = []; if(!$this->model->isCoach($idTeam, $session->getAccount()->getUser()->getEmail())) {
return ViewHttpResponse::twig('error.html.twig', [
$request = HttpRequest::from($request, $errors, [ 'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette action pour cette équipe.")],
"team" => [Validators::isInteger()], ], HttpCodes::FORBIDDEN);
"email" => [Validators::email(), Validators::lenBetween(5, 256)], }
$failures = [];
$request = HttpRequest::from($request, $failures, [
"name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()],
"main_color" => [Validators::hexColor()],
"second_color" => [Validators::hexColor()],
"picture" => [Validators::isURL()],
]); ]);
if (!empty($failures)) {
return $this->displayTeam($this->model->deleteMember($request['email'], intval($request['team'])), $session); $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);
} }
} }

@ -16,6 +16,7 @@ class UserController {
/** /**
* @param TacticModel $tactics * @param TacticModel $tactics
* @param TeamModel|null $teams
*/ */
public function __construct(TacticModel $tactics, ?TeamModel $teams = null) { public function __construct(TacticModel $tactics, ?TeamModel $teams = null) {
$this->tactics = $tactics; $this->tactics = $tactics;
@ -28,13 +29,15 @@ class UserController {
*/ */
public function home(SessionHandle $session): ViewHttpResponse { public function home(SessionHandle $session): ViewHttpResponse {
$limitNbTactics = 5; $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) { if ($this->teams != null) {
$teams = $this->teams->getAll($session->getAccount()->getId()); $teams = $this->teams->getAll($user->getId());
} else { } else {
$teams = []; $teams = [];
} }
@ -45,18 +48,13 @@ class UserController {
"teams" => $teams, "teams" => $teams,
"username" => $name, "username" => $name,
]); ]);
// return ViewHttpResponse::react("views/Home.tsx", []);
}
public function homeTwig(SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("home.twig", []);
} }
/** /**
* @return ViewHttpResponse account settings page * @return ViewHttpResponse account settings page
*/ */
public function settings(SessionHandle $session): ViewHttpResponse { public function settings(SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("account_settings.twig", []); return ViewHttpResponse::react("views/Settings.tsx", []);
} }
public function disconnect(MutableSessionHandle $session): HttpResponse { public function disconnect(MutableSessionHandle $session): HttpResponse {

@ -28,7 +28,7 @@ class VisualizerController {
public function openVisualizer(int $id, SessionHandle $session): HttpResponse { public function openVisualizer(int $id, SessionHandle $session): HttpResponse {
$tactic = $this->tacticModel->get($id); $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) { if ($failure != null) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);

@ -67,28 +67,43 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.failed{
color: red;
}
</style> </style>
</head> </head>
<body> <body>
<header>
<h1><a href="{{ path('/') }}">IQBall</a></h1>
</header>
<div class="container"> <div class="container">
<h2>Ajouter un membre à votre équipe</h2> <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"> <div class="form-group">
<label for="team">Team où ajouter le membre :</label>
<input type="text" id="team" name="team" required> <label for="email">Email du membre :</label>
<label for="mail">Email du membre :</label> {% if badEmail %}
<input type="text" id="mail" name="mail" required> <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"> <fieldset class="role">
<legend>Rôle du membre dans l'équipe :</legend> <legend>Rôle du membre dans l'équipe :</legend>
<div class="radio"> <div class="radio">
<label for="P">Joueur</label> <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>
<div class="radio"> <div class="radio">
<label for="C">Coach</label> <label for="C">Coach</label>
<input type="radio" id="C" name="role" value="C" /> <input type="radio" id="C" name="role" value="COACH" />
</div> </div>
</fieldset> </fieldset>

@ -11,10 +11,6 @@
align-items: center; align-items: center;
} }
section {
width: 60%;
}
.square { .square {
width: 50px; width: 50px;
height: 50px; height: 50px;
@ -30,19 +26,17 @@
border: solid; border: solid;
} }
.container { section {
background-color: #fff; background-color: #fff;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 60%;
} }
.team { #colors{
border-color: darkgrey; flex-direction: row;
border-radius: 20px;
} }
.color { .color {
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
@ -53,6 +47,16 @@
width: 80px; width: 80px;
} }
#delete{
border-radius:10px ;
background-color: red;
color: white;
}
.player{
flex-direction: row;
justify-content: space-evenly;
}
</style> </style>
</head> </head>
<body> <body>
@ -61,13 +65,18 @@
</header> </header>
<section class="container"> <section class="container">
{% if notDeleted %}
<div class="team container"> <popup>
<p>Cette équipe ne peut être supprimée.</p>
</popup>
{% endif %}
{% if team is defined %}
<div class="team">
<div> <div>
<h1>{{ team.getInfo().getName() }}</h1> <h1>{{ team.getInfo().getName() }}</h1>
<img src="{{ team.getInfo().getPicture() }}" alt="Logo d'équipe" class="logo"> <img src="{{ team.getInfo().getPicture() }}" alt="Logo d'équipe" class="logo">
</div> </div>
<div> <div id="colors">
<div class="color"><p>Couleur principale : </p> <div class="color"><p>Couleur principale : </p>
<div class="square" id="main_color"></div> <div class="square" id="main_color"></div>
</div> </div>
@ -75,17 +84,26 @@
<div class="square" id="second_color"></div> <div class="square" id="second_color"></div>
</div> </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() %} {% for m in team.listMembers() %}
<div class="player">
<p> {{ m.getUserId() }} </p> <p> {{ m.getUserId() }} </p>
{% if m.getRole().isCoach() %} {% if m.getRole().isCoach() %}
<p> : Coach</p> <p> : Coach</p>
{% else %} {% else %}
<p> : Joueur</p> <p> : Joueur</p>
{% endif %} {% endif %}
</div>
{% endfor %} {% endfor %}
</div> </div>
{% else %}
<div>
<h3>Cette équipe ne peut être affichée</h3>
</div>
{% endif %}
</section> </section>
</body> </body>
</html> </html>

@ -3,9 +3,37 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Twig view</title> <title>Twig view</title>
<style>
body {
display: flex;
flex-direction: column;
background-color: #f1f1f1;
align-items: center;
}
section{
flex-direction: row;
justify-content: space-around;
background-color: white;
width: 60%;
}
.team {
border-radius: 10px;
border-color: darkgrey;
}
.logo_team {
width: 15%;
aspect-ratio: 3/2;
object-fit: contain;
}
</style>
</head> </head>
<body> <body>
<header>
<h1><a href="{{ path('/') }}">IQBall</a></h1>
</header>
<section>
{% if teams is empty %} {% if teams is empty %}
<p>Aucune équipe n'a été trouvée</p> <p>Aucune équipe n'a été trouvée</p>
<div class="container"> <div class="container">
@ -22,12 +50,12 @@
</div> </div>
{% else %} {% else %}
{% for t in teams %} {% for t in teams %}
<div class="team" onclick="window.location.href = '{{ path("/team/#{t.id}") }}'"> <div class="team" onclick="window.location.href = '{{ path("/team/#{t.getId()}") }}'">
<p>Nom de l'équipe : {{ t.name }}</p> <p>Nom de l'équipe : {{ t.getName() }}</p>
<img src="{{ t.picture }}" alt="logo de l'équipe"> <img src="{{ t.getPicture() }}" alt="logo de l'équipe" class="logo_team">
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</section>
</body> </body>
</html> </html>

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Insertion view</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f1f1f1;
}
.container {
max-width: 400px;
margin: 5px auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
{% for item in bad_fields %}
#{{ item }}{
border-color: red;
}{% endfor %} input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="submit"] {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h2>Modifier votre équipe</h2>
<form action="{{ path('/team/' ~ team.getInfo().getId() ~ '/edit') }}" method="post">
<div class="form-group">
<label for="name">Nom de l'équipe :</label>
<input type="text" id="name" name="name" value="{{ team.getInfo().getName() }}" required>
<label for="picture">Logo:</label>
<input type="text" id="picture" name="picture" value="{{ team.getInfo().getPicture() }}" required>
<label for="main_color">Couleur principale</label>
<input type="color" value="{{ team.getInfo().getMainColor() }}" id="main_color" name="main_color" required>
<label for="second_color">Couleur secondaire</label>
<input type="color" id="second_color" name="second_color" value="{{ team.getInfo().getSecondColor() }}" required>
</div>
<div class="form-group">
<input type="submit" value="Confirmer">
</div>
</form>
</div>
</body>
</html>

@ -74,7 +74,7 @@
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
<p>Aucune équipe créé !</p> <p>Aucune équipe créée !</p>
{% endif %} {% endif %}
<h2> Mes strategies </h2> <h2> Mes strategies </h2>
@ -90,7 +90,7 @@
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
<p> Aucune tactique créé !</p> <p> Aucune tactique créée !</p>
{% endif %} {% endif %}
</body> </body>

@ -54,7 +54,6 @@
background-color: #0056b3; background-color: #0056b3;
} }
</style> </style>
</head> </head>
<body> <body>
@ -68,7 +67,7 @@
<label for="picture">Logo:</label> <label for="picture">Logo:</label>
<input type="text" id="picture" name="picture" required> <input type="text" id="picture" name="picture" required>
<label for="main_color">Couleur principale</label> <label for="main_color">Couleur principale</label>
<input type="color" id="main_color" name="main_color" required> <input type="color" value="#ffffff" id="main_color" name="main_color" required>
<label for="second_color">Couleur secondaire</label> <label for="second_color">Couleur secondaire</label>
<input type="color" id="second_color" name="second_color" required> <input type="color" id="second_color" name="second_color" required>
</div> </div>

@ -7,6 +7,9 @@
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
background-color: #f1f1f1; background-color: #f1f1f1;
display: flex;
flex-direction: column;
align-items: center;
} }
.container { .container {
@ -56,7 +59,9 @@
</style> </style>
</head> </head>
<body> <body>
<header>
<h1><a href="{{ path('/') }}">IQBall</a></h1>
</header>
<div class="container"> <div class="container">
<h2>Chercher une équipe</h2> <h2>Chercher une équipe</h2>
<form action="{{ path('/team/search') }}" method="post"> <form action="{{ path('/team/search') }}" method="post">

@ -8,54 +8,34 @@ namespace IQBall\Core\Data;
* to share to other users, or non-needed public information * to share to other users, or non-needed public information
*/ */
class Account { class Account {
/**
* @var string $email account's mail address
*/
private string $email;
/** /**
* @var string string token * @var string string token
*/ */
private string $token; private string $token;
/** /**
* @var string the account's username * @var User contains all the account's "public" information
*/ */
private string $name; private User $user;
/**
* @var int
*/
private int $id;
/** /**
* @param string $email
* @param string $name
* @param string $token * @param string $token
* @param int $id * @param User $user
*/ */
public function __construct(string $email, string $name, string $token, int $id) { public function __construct(string $token, User $user) {
$this->email = $email;
$this->name = $name;
$this->token = $token; $this->token = $token;
$this->id = $id; $this->user = $user;
}
public function getId(): int {
return $this->id;
}
public function getEmail(): string {
return $this->email;
} }
public function getToken(): string { public function getToken(): string {
return $this->token; return $this->token;
} }
public function getName(): string { /**
return $this->name; * @return User
*/
public function getUser(): User {
return $this->user;
} }
} }

@ -1,44 +0,0 @@
<?php
namespace IQBall\Core\Data;
use InvalidArgumentException;
class Color {
/**
* @var string that represents an hexadecimal color code
*/
private string $hex;
/**
* @param string $value 6 bytes unsigned int that represents an RGB color
* @throws InvalidArgumentException if the value is negative or greater than 0xFFFFFF
*/
private function __construct(string $value) {
$this->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);
}
}

@ -5,11 +5,8 @@ namespace IQBall\Core\Data;
/** /**
* information about a team member * information about a team member
*/ */
class Member { class Member implements \JsonSerializable {
/** private User $user;
* @var int The member's user account
*/
private int $userId;
/** /**
* @var int The member's team id * @var int The member's team id
@ -17,32 +14,25 @@ class Member {
private int $teamId; 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 User $user
* @param MemberRole $role * @param int $teamId
* @param string $role
*/ */
public function __construct(int $userId, int $teamId, MemberRole $role) { public function __construct(User $user, int $teamId, string $role) {
$this->userId = $userId; $this->user = $user;
$this->teamId = $teamId; $this->teamId = $teamId;
$this->role = $role; $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; return $this->role;
} }
@ -52,4 +42,16 @@ class Member {
public function getTeamId(): int { public function getTeamId(): int {
return $this->teamId; return $this->teamId;
} }
/**
* @return User
*/
public function getUser(): User {
return $this->user;
}
public function jsonSerialize() {
return get_object_vars($this);
}
} }

@ -1,68 +0,0 @@
<?php
namespace IQBall\Core\Data;
use InvalidArgumentException;
/**
* Enumeration class workaround
* As there is no enumerations in php 7.4, this class
* encapsulates an integer value and use it as a variant discriminant
*/
final class MemberRole {
private const ROLE_PLAYER = 0;
private const ROLE_COACH = 1;
private const MIN = self::ROLE_PLAYER;
private const MAX = self::ROLE_COACH;
private int $value;
private function __construct(int $val) {
if (!$this->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);
}
}

@ -2,7 +2,7 @@
namespace IQBall\Core\Data; namespace IQBall\Core\Data;
class Team { class Team implements \JsonSerializable {
private TeamInfo $info; private TeamInfo $info;
/** /**
@ -29,4 +29,10 @@ class Team {
public function listMembers(): array { public function listMembers(): array {
return $this->members; return $this->members;
} }
public function jsonSerialize() {
return get_object_vars($this);
}
} }

@ -2,21 +2,21 @@
namespace IQBall\Core\Data; namespace IQBall\Core\Data;
class TeamInfo { class TeamInfo implements \JsonSerializable {
private int $id; private int $id;
private string $name; private string $name;
private string $picture; private string $picture;
private Color $mainColor; private string $mainColor;
private Color $secondColor; private string $secondColor;
/** /**
* @param int $id * @param int $id
* @param string $name * @param string $name
* @param string $picture * @param string $picture
* @param Color $mainColor * @param string $mainColor
* @param Color $secondColor * @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->id = $id;
$this->name = $name; $this->name = $name;
$this->picture = $picture; $this->picture = $picture;
@ -37,13 +37,17 @@ class TeamInfo {
return $this->picture; return $this->picture;
} }
public function getMainColor(): Color { public function getMainColor(): string {
return $this->mainColor; return $this->mainColor;
} }
public function getSecondColor(): Color { public function getSecondColor(): string {
return $this->secondColor; return $this->secondColor;
} }
public function jsonSerialize() {
return get_object_vars($this);
}
} }

@ -0,0 +1,72 @@
<?php
namespace IQBall\Core\Data;
use _PHPStan_4c4f22f13\Nette\Utils\Json;
class User implements \JsonSerializable {
/**
* @var string $email user's mail address
*/
private string $email;
/**
* @var string the user's username
*/
private string $name;
/**
* @var int the user's id
*/
private int $id;
/**
* @var string user's profile picture
*/
private string $profilePicture;
/**
* @param string $email
* @param string $name
* @param int $id
* @param string $profilePicture
*/
public function __construct(string $email, string $name, int $id, string $profilePicture) {
$this->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);
}
}

@ -4,6 +4,7 @@ namespace IQBall\Core\Gateway;
use IQBall\Core\Connection; use IQBall\Core\Connection;
use IQBall\Core\Data\Account; use IQBall\Core\Data\Account;
use IQBall\Core\Data\User;
use PDO; use PDO;
class AccountGateway { class AccountGateway {
@ -16,13 +17,13 @@ class AccountGateway {
$this->con = $con; $this->con = $con;
} }
public function insertAccount(string $name, string $email, string $token, string $hash, string $profilePicture): int {
public function insertAccount(string $name, string $email, string $token, string $hash): int { $this->con->exec("INSERT INTO Account(username, hash, email, token,profilePicture) VALUES (:username,:hash,:email,:token,:profilePic)", [
$this->con->exec("INSERT INTO Account(username, hash, email, token) VALUES (:username,:hash,:email,:token)", [
':username' => [$name, PDO::PARAM_STR], ':username' => [$name, PDO::PARAM_STR],
':hash' => [$hash, PDO::PARAM_STR], ':hash' => [$hash, PDO::PARAM_STR],
':email' => [$email, PDO::PARAM_STR], ':email' => [$email, PDO::PARAM_STR],
':token' => [$token, PDO::PARAM_STR], ':token' => [$token, PDO::PARAM_STR],
':profilePic' => [$profilePicture, PDO::PARAM_STR],
]); ]);
return intval($this->con->lastInsertId()); return intval($this->con->lastInsertId());
} }
@ -65,7 +66,7 @@ class AccountGateway {
return null; 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 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"]));
} }

@ -4,7 +4,7 @@ namespace IQBall\Core\Gateway;
use IQBall\Core\Connection; use IQBall\Core\Connection;
use IQBall\Core\Data\Member; use IQBall\Core\Data\Member;
use IQBall\Core\Data\MemberRole; use IQBall\Core\Data\User;
use PDO; use PDO;
class MemberGateway { class MemberGateway {
@ -41,13 +41,12 @@ class MemberGateway {
*/ */
public function getMembersOfTeam(int $teamId): array { public function getMembersOfTeam(int $teamId): array {
$rows = $this->con->fetch( $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], ":id" => [$teamId, PDO::PARAM_INT],
] ]
); );
return array_map(fn($row) => new Member(new User($row['email'], $row['username'], $row['id'], $row['profilePicture']), $teamId, $row['role']), $rows);
return array_map(fn($row) => new Member($row['id_user'], $row['id_team'], MemberRole::fromName($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);
}
} }

@ -3,7 +3,6 @@
namespace IQBall\Core\Gateway; namespace IQBall\Core\Gateway;
use IQBall\Core\Connection; use IQBall\Core\Connection;
use IQBall\Core\Data\Color;
use IQBall\Core\Data\TeamInfo; use IQBall\Core\Data\TeamInfo;
use PDO; use PDO;
@ -23,7 +22,7 @@ class TeamGateway {
*/ */
public function insert(string $name, string $picture, string $mainColor, string $secondColor): int { public function insert(string $name, string $picture, string $mainColor, string $secondColor): int {
$this->con->exec( $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], ":team_name" => [$name, PDO::PARAM_STR],
":picture" => [$picture, PDO::PARAM_STR], ":picture" => [$picture, PDO::PARAM_STR],
@ -34,29 +33,29 @@ class TeamGateway {
return intval($this->con->lastInsertId()); return intval($this->con->lastInsertId());
} }
/** /**
* @param string $name * @param string $name
* @param int $id
* @return TeamInfo[] * @return TeamInfo[]
*/ */
public function listByName(string $name): array { public function listByName(string $name, int $id): array {
$result = $this->con->fetch( $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], ":name" => [$name, PDO::PARAM_STR],
"id" => [$id, PDO::PARAM_INT],
] ]
); );
return array_map(fn($row) => new TeamInfo($row['id'], $row['name'], $row['picture'], $row['main_color'], $row['second_color']), $result);
return array_map(fn($row) => new TeamInfo($row['id'], $row['name'], $row['picture'], Color::from($row['main_color']), Color::from($row['second_color'])), $result);
} }
/** /**
* @param int $id * @param int $id
* @return TeamInfo * @return TeamInfo|null
*/ */
public function getTeamById(int $id): ?TeamInfo { public function getTeamById(int $id): ?TeamInfo {
$row = $this->con->fetch( $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],
] ]
@ -64,8 +63,7 @@ class TeamGateway {
if ($row == null) { if ($row == null) {
return null; return null;
} }
return new TeamInfo($row['id'], $row['name'], $row['picture'], $row['main_color'], $row['second_color']);
return new TeamInfo($row['id'], $row['name'], $row['picture'], Color::from($row['main_color']), Color::from($row['second_color']));
} }
/** /**
@ -74,7 +72,7 @@ class TeamGateway {
*/ */
public function getTeamIdByName(string $name): ?int { public function getTeamIdByName(string $name): ?int {
return $this->con->fetch( 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],
] ]
@ -82,13 +80,62 @@ class TeamGateway {
} }
/** /**
* Undocumented function * @param int $idTeam
* */
* @param integer $user public function deleteTeam(int $idTeam): void {
* @return array<array<string, mixed>> $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<string, mixed>
*/ */
public function getAll(int $user): 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],
]
);
} }
} }

@ -42,11 +42,24 @@ class HttpResponse {
* @param int $code only HTTP 3XX codes are accepted. * @param int $code only HTTP 3XX codes are accepted.
* @return HttpResponse a response that will redirect client to given url * @return HttpResponse a response that will redirect client to given url
*/ */
public static function redirect(string $url, int $code = HttpCodes::FOUND): HttpResponse { 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) { if ($code < 300 || $code >= 400) {
throw new \InvalidArgumentException("given code is not a redirection http code"); throw new \InvalidArgumentException("given code is not a redirection http code");
} }
return new HttpResponse($code, ["Location" => $url]); return new HttpResponse($code, ["Location" => $url]);
} }
} }

@ -2,13 +2,16 @@
namespace IQBall\Core\Model; namespace IQBall\Core\Model;
use Exception;
use IQBall\Core\Data\Account; use IQBall\Core\Data\Account;
use IQBall\Core\Data\User;
use IQBall\Core\Gateway\AccountGateway; use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\FieldValidationFail;
use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\ValidationFail;
class AuthModel { class AuthModel {
private AccountGateway $gateway; 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 * @param AccountGateway $gateway
@ -17,7 +20,6 @@ class AuthModel {
$this->gateway = $gateway; $this->gateway = $gateway;
} }
/** /**
* @param string $username * @param string $username
* @param string $password * @param string $password
@ -25,6 +27,7 @@ class AuthModel {
* @param string $email * @param string $email
* @param ValidationFail[] $failures * @param ValidationFail[] $failures
* @return Account|null the registered account or null if failures occurred * @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 { 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); $hash = password_hash($password, PASSWORD_DEFAULT);
$token = $this->generateToken(); $token = $this->generateToken();
$accountId = $this->gateway->insertAccount($username, $email, $token, $hash); $accountId = $this->gateway->insertAccount($username, $email, $token, $hash, self::DEFAULT_PROFILE_PICTURE);
return new Account($email, $username, $token, $accountId); return new Account($token, new User($email, $username, $accountId, self::DEFAULT_PROFILE_PICTURE));
} }
/** /**
* Generate a random base 64 string * Generate a random base 64 string
* @return string * @return string
* @throws Exception
*/ */
private function generateToken(): string { private function generateToken(): string {
return base64_encode(random_bytes(64)); return base64_encode(random_bytes(64));
@ -70,5 +74,4 @@ class AuthModel {
return $this->gateway->getAccountFromMail($email); return $this->gateway->getAccountFromMail($email);
} }
} }

@ -2,7 +2,6 @@
namespace IQBall\Core\Model; namespace IQBall\Core\Model;
use IQBall\Core\Data\Color;
use IQBall\Core\Data\Team; use IQBall\Core\Data\Team;
use IQBall\Core\Data\TeamInfo; use IQBall\Core\Data\TeamInfo;
use IQBall\Core\Gateway\AccountGateway; use IQBall\Core\Gateway\AccountGateway;
@ -26,6 +25,7 @@ class TeamModel {
} }
/** /**
* Create a team
* @param string $name * @param string $name
* @param string $picture * @param string $picture
* @param string $mainColor * @param string $mainColor
@ -37,48 +37,99 @@ class TeamModel {
} }
/** /**
* adds a member to a team * add a member to a team
* @param string $mail * @param string $mail
* @param int $teamId * @param int $teamId
* @param string $role * @param string $role
* @return void * @return int
*/ */
public function addMember(string $mail, int $teamId, string $role): void { public function addMember(string $mail, int $teamId, string $role): int {
$userId = $this->users->getAccountFromMail($mail)->getId(); $user = $this->users->getAccountFromMail($mail);
$this->members->insert($teamId, $userId, $role); 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 string $name
* @param int $id
* @return TeamInfo[] * @return TeamInfo[]
*/ */
public function listByName(string $name): array { public function listByName(string $name, int $id): array {
return $this->teams->listByName($name); return $this->teams->listByName($name, $id);
} }
/** /**
* @param int $id * @param int $idTeam
* @return Team * @param int $idCurrentUser
* @return Team|null
*/ */
public function getTeam(int $id): Team { public function getTeam(int $idTeam, int $idCurrentUser): ?Team {
$teamInfo = $this->teams->getTeamById($id); if(!$this->members->isMemberOfTeam($idTeam, $idCurrentUser)) {
$members = $this->members->getMembersOfTeam($id); return null;
}
$teamInfo = $this->teams->getTeamById($idTeam);
$members = $this->members->getMembersOfTeam($idTeam);
return new Team($teamInfo, $members); return new Team($teamInfo, $members);
} }
/** /**
* delete a member from given team identifier * delete a member from given team identifier
* @param string $mail * @param int $idMember
* @param int $teamId * @param int $teamId
* @return int * @return int
*/ */
public function deleteMember(string $mail, int $teamId): int { public function deleteMember(int $idMember, int $teamId): int {
$userId = $this->users->getAccountFromMail($mail)->getId(); $this->members->remove($teamId, $idMember);
$this->members->remove($teamId, $userId); if(empty($this->members->getMembersOfTeam($teamId))) {
$this->teams->deleteTeam($teamId);
return -1;
}
return $teamId; 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 * Get all user's teams
* *
@ -88,5 +139,4 @@ class TeamModel {
public function getAll(int $user): array { public function getAll(int $user): array {
return $this->teams->getAll($user); return $this->teams->getAll($user);
} }
} }

Loading…
Cancel
Save