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
.vscode
.idea
.code
.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.
* @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);

@ -1,5 +1,4 @@
<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="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"/>

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 computeInternalSegments = useCallback((segments: Segment[]) => {
const computeInternalSegments = useCallback(
(segments: Segment[]) => {
return segments.map((segment, idx) => {
if (idx == 0) {
return {
@ -132,7 +133,9 @@ export default function BendableArrow({
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<HTMLDivElement>(null)
const tailRef = useRef<HTMLDivElement>(null)
/**
* Computes and return the segments edition points
* @param parentBase

@ -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 && (

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

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

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

@ -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.",
}
}

@ -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 { Segment } from "../components/arrows/BendableArrow"
import { Pos } from "../../components/arrows/Pos"
import { Segment } from "../../components/arrows/BendableArrow"
import { PlayerId } from "./Player"
export enum ActionKind {

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

@ -1,5 +1,5 @@
import { Player } from "./Player"
import { CourtObject } from "./CourtObjects"
import { CourtObject } from "./Ball"
import { Action } from "./Action"
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 {
position: relative;
background-color: white;
height: 100%;
width: 100%;
height: 80vh;
}
.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);
}
.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;
}

@ -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 { 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<RackedCourtObject[]>(
@ -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 (
<div id="main-div">
<div id="topbar-div">
<button onClick={() => (location.pathname = BASE + "/")}>
Home
</button>
<div id="topbar-left">
<SavingState state={saveState} />
</div>
@ -539,7 +543,7 @@ function renderCourtObject(courtObject: RackedCourtObject) {
if (courtObject.key == "ball") {
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 }) {
@ -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) =>

@ -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 (
<div id="main">
<Title username={username} />
<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
}

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

@ -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 { RefObject } from "react"
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();
return $session->getAccount();
}
$token = $headers['Authorization'];
$gateway = new AccountGateway(new Connection(get_database()));
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("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;
}

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

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

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

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

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

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

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

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

@ -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() %}
<div class="player">
<p> {{ m.getUserId() }} </p>
{% if m.getRole().isCoach() %}
<p> : Coach</p>
{% else %}
<p> : Joueur</p>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div>
<h3>Cette équipe ne peut être affichée</h3>
</div>
{% endif %}
</section>
</body>
</html>

@ -3,9 +3,37 @@
<head>
<meta charset="UTF-8">
<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>
<body>
<header>
<h1><a href="{{ path('/') }}">IQBall</a></h1>
</header>
<section>
{% if teams is empty %}
<p>Aucune équipe n'a été trouvée</p>
<div class="container">
@ -22,12 +50,12 @@
</div>
{% else %}
{% for t in teams %}
<div class="team" onclick="window.location.href = '{{ path("/team/#{t.id}") }}'">
<p>Nom de l'équipe : {{ t.name }}</p>
<img src="{{ t.picture }}" alt="logo de l'équipe">
<div class="team" onclick="window.location.href = '{{ path("/team/#{t.getId()}") }}'">
<p>Nom de l'équipe : {{ t.getName() }}</p>
<img src="{{ t.getPicture() }}" alt="logo de l'équipe" class="logo_team">
</div>
{% endfor %}
{% endif %}
</section>
</body>
</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>
{% endfor %}
{% else %}
<p>Aucune équipe créé !</p>
<p>Aucune équipe créée !</p>
{% endif %}
<h2> Mes strategies </h2>
@ -90,7 +90,7 @@
</div>
{% endfor %}
{% else %}
<p> Aucune tactique créé !</p>
<p> Aucune tactique créée !</p>
{% endif %}
</body>

@ -54,7 +54,6 @@
background-color: #0056b3;
}
</style>
</head>
<body>
@ -68,7 +67,7 @@
<label for="picture">Logo:</label>
<input type="text" id="picture" name="picture" required>
<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>
<input type="color" id="second_color" name="second_color" required>
</div>

@ -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 @@
</style>
</head>
<body>
<header>
<h1><a href="{{ path('/') }}">IQBall</a></h1>
</header>
<div class="container">
<h2>Chercher une équipe</h2>
<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
*/
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;
}
}

@ -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
*/
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);
}
}

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

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

@ -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\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"]));
}

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

@ -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,29 +33,29 @@ 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],
]
@ -64,8 +63,7 @@ class TeamGateway {
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,7 +72,7 @@ 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],
]
@ -82,13 +80,62 @@ class TeamGateway {
}
/**
* Undocumented function
*
* @param integer $user
* @return array<array<string, mixed>>
* @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<string, mixed>
*/
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.
* @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]);
}
}

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

@ -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<array<string, mixed>>
*/
public function getAll(int $user) : array {
public function getAll(int $user): array {
return $this->teams->getAll($user);
}
}

Loading…
Cancel
Save