conflicts
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
commit
dac0c54cec
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
$finder = (new PhpCsFixer\Finder())->in(__DIR__);
|
||||
|
||||
return (new PhpCsFixer\Config())
|
||||
->setRules([
|
||||
'@PER-CS' => true,
|
||||
'@PHP74Migration' => true,
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
'braces_position' => [
|
||||
'classes_opening_brace' => 'same_line',
|
||||
'functions_opening_brace' => 'same_line'
|
||||
]
|
||||
])
|
||||
->setIndent(" ")
|
||||
->setFinder($finder);
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"bracketSameLine": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 80,
|
||||
"tabWidth": 4,
|
||||
"semi": false
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
# The wiki also exists
|
||||
|
||||
Some of our explanation are contained in the [wiki](https://codefirst.iut.uca.fr/git/IQBall/Application-Web/wiki)
|
@ -1,12 +0,0 @@
|
||||
@startuml
|
||||
|
||||
class Connexion
|
||||
|
||||
class Modele
|
||||
|
||||
class Account
|
||||
|
||||
class AccountGateway
|
||||
|
||||
|
||||
@enduml
|
@ -0,0 +1,32 @@
|
||||
@startuml
|
||||
|
||||
class AuthController {
|
||||
+__construct (model : AuthModel)
|
||||
+ displayRegister() : HttpResponse
|
||||
+ register(request : array,session : MutableSessionHandle) : HttpResponse
|
||||
+ displayLogin() : HttpResponse
|
||||
+ login(request : array , session : MutableSessionHandle) : HttpResponse
|
||||
}
|
||||
AuthController --> "- model" AuthModel
|
||||
|
||||
class AuthModel {
|
||||
+__construct(gateway : AccountGateway)
|
||||
+ register(username : string, password : string, confirmPassword : string, email : string, failures : array): Account
|
||||
+ generateToken() : string
|
||||
+ login(email : string, password : string)
|
||||
}
|
||||
AuthModel --> "- gateway" AccountGateway
|
||||
|
||||
class AccountGateway {
|
||||
-con : Connection
|
||||
+__construct(con : Connection)
|
||||
+ insertAccount(name : string, email : string, hash : string, token : string) : int
|
||||
+ getRowsFromMail(email : string): array
|
||||
+ getHash(email : string) : array
|
||||
+ exists(email : string) : bool
|
||||
+ getAccountFromMail(email : string ): Account
|
||||
+ getAccountFromToken(email : string ): Account
|
||||
|
||||
}
|
||||
|
||||
@enduml
|
@ -0,0 +1,63 @@
|
||||
@startuml
|
||||
class Team {
|
||||
- name: string
|
||||
- picture: Url
|
||||
- members: array<int, MemberRole>
|
||||
|
||||
+ __construct(name : string, picture : string, mainColor : Colo, secondColor : Color)
|
||||
+ getName(): string
|
||||
+ getPicture(): Url
|
||||
+ getMainColor(): Color
|
||||
+ getSecondColor(): Color
|
||||
+ listMembers(): array<Member>
|
||||
}
|
||||
|
||||
Team --> "- mainColor" Color
|
||||
Team --> "- secondColor" Color
|
||||
|
||||
class Color {
|
||||
- value: string
|
||||
- __construct(value : string)
|
||||
+ getValue(): string
|
||||
+ from(value: string): Color
|
||||
+ tryFrom(value : string) : ?Color
|
||||
}
|
||||
|
||||
class TeamGateway{
|
||||
--
|
||||
+ __construct(con : Connexion)
|
||||
+ insert(name : string ,picture : string, mainColor : Color, secondColor : Color)
|
||||
+ listByName(name : string): array
|
||||
}
|
||||
|
||||
TeamGateway *--"- con" Connexion
|
||||
TeamGateway ..> Color
|
||||
|
||||
class TeamModel{
|
||||
---
|
||||
+ __construct(gateway : TeamGateway)
|
||||
+ createTeam(name : string,picture : string, mainColorValue : int, secondColorValue : int, errors : array)
|
||||
+ listByName(name : string ,errors : array) : ?array
|
||||
+ displayTeam(id : int): Team
|
||||
}
|
||||
|
||||
TeamModel *--"- gateway" TeamGateway
|
||||
TeamModel ..> Team
|
||||
TeamModel ..> Color
|
||||
|
||||
class TeamController{
|
||||
- twig : Environement
|
||||
--
|
||||
+ __construct( model : TeamModel, twig : Environement)
|
||||
+ displaySubmitTeam() : HttpResponse
|
||||
+ submitTeam(request : array) : HttpResponse
|
||||
+ displayListTeamByName(): HttpResponse
|
||||
+ listTeamByName(request : array) : HttpResponse
|
||||
+ displayTeam(id : int): HttpResponse
|
||||
}
|
||||
|
||||
TeamController *--"- model" TeamModel
|
||||
|
||||
class Connexion { }
|
||||
|
||||
@enduml
|
@ -1,3 +1,7 @@
|
||||
# IQBall - Web Application
|
||||
This repository hosts the IQBall application for web
|
||||
|
||||
## Read the docs !
|
||||
You can find some additional documentation in the [Documentation](Documentation) folder,
|
||||
and in the [wiki](https://codefirst.iut.uca.fr/git/IQBall/Application-Web/wiki).
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## verify php and typescript types
|
||||
|
||||
echo "formatting php typechecking"
|
||||
vendor/bin/php-cs-fixer fix
|
||||
|
||||
echo "formatting typescript typechecking"
|
||||
npm run format
|
@ -1,4 +1,4 @@
|
||||
/**
|
||||
* This constant defines the API endpoint.
|
||||
*/
|
||||
export const API = import.meta.env.VITE_API_ENDPOINT;
|
||||
export const API = import.meta.env.VITE_API_ENDPOINT
|
||||
|
@ -0,0 +1,16 @@
|
||||
import { API } from "./Constants"
|
||||
|
||||
export function fetchAPI(
|
||||
url: string,
|
||||
payload: unknown,
|
||||
method = "POST",
|
||||
): Promise<Response> {
|
||||
return fetch(`${API}/${url}`, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
export function calculateRatio(
|
||||
it: { x: number; y: number },
|
||||
parent: DOMRect,
|
||||
): { x: number; y: number } {
|
||||
const relativeXPixels = it.x - parent.x
|
||||
const relativeYPixels = it.y - parent.y
|
||||
|
||||
const xRatio = relativeXPixels / parent.width
|
||||
const yRatio = relativeYPixels / parent.height
|
||||
|
||||
return { x: xRatio, y: yRatio }
|
||||
}
|
After Width: | Height: | Size: 507 B |
After Width: | Height: | Size: 732 B |
@ -1,58 +1,72 @@
|
||||
import {ReactElement, useRef} from "react";
|
||||
import Draggable from "react-draggable";
|
||||
import { ReactElement, useRef } from "react"
|
||||
import Draggable from "react-draggable"
|
||||
|
||||
export interface RackProps<E> {
|
||||
id: string,
|
||||
objects: E[],
|
||||
onChange: (objects: E[]) => void,
|
||||
canDetach: (ref: HTMLDivElement) => boolean,
|
||||
onElementDetached: (ref: HTMLDivElement, el: E) => void,
|
||||
render: (e: E) => ReactElement,
|
||||
export interface RackProps<E extends { key: string | number }> {
|
||||
id: string
|
||||
objects: E[]
|
||||
onChange: (objects: E[]) => void
|
||||
canDetach: (ref: HTMLDivElement) => boolean
|
||||
onElementDetached: (ref: HTMLDivElement, el: E) => void
|
||||
render: (e: E) => ReactElement
|
||||
}
|
||||
|
||||
interface RackItemProps<E> {
|
||||
item: E,
|
||||
onTryDetach: (ref: HTMLDivElement, el: E) => void,
|
||||
render: (e: E) => ReactElement,
|
||||
interface RackItemProps<E extends { key: string | number }> {
|
||||
item: E
|
||||
onTryDetach: (ref: HTMLDivElement, el: E) => void
|
||||
render: (e: E) => ReactElement
|
||||
}
|
||||
|
||||
/**
|
||||
* A container of draggable objects
|
||||
* */
|
||||
export function Rack<E>({id, objects, onChange, canDetach, onElementDetached, render}: RackProps<E>) {
|
||||
export function Rack<E extends { key: string | number }>({
|
||||
id,
|
||||
objects,
|
||||
onChange,
|
||||
canDetach,
|
||||
onElementDetached,
|
||||
render,
|
||||
}: RackProps<E>) {
|
||||
return (
|
||||
<div id={id} style={{
|
||||
display: "flex"
|
||||
}}>
|
||||
{objects.map(element => (
|
||||
<RackItem key={element.key}
|
||||
item={element}
|
||||
render={render}
|
||||
onTryDetach={(ref, element) => {
|
||||
if (!canDetach(ref))
|
||||
return
|
||||
<div
|
||||
id={id}
|
||||
style={{
|
||||
display: "flex",
|
||||
}}>
|
||||
{objects.map((element) => (
|
||||
<RackItem
|
||||
key={element.key}
|
||||
item={element}
|
||||
render={render}
|
||||
onTryDetach={(ref, element) => {
|
||||
if (!canDetach(ref)) return
|
||||
|
||||
const index = objects.findIndex(o => o.key === element.key)
|
||||
onChange(objects.toSpliced(index, 1))
|
||||
const index = objects.findIndex(
|
||||
(o) => o.key === element.key,
|
||||
)
|
||||
onChange(objects.toSpliced(index, 1))
|
||||
|
||||
onElementDetached(ref, element)
|
||||
}}/>
|
||||
onElementDetached(ref, element)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RackItem<E>({item, onTryDetach, render}: RackItemProps<E>) {
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
function RackItem<E extends { key: string | number }>({
|
||||
item,
|
||||
onTryDetach,
|
||||
render,
|
||||
}: RackItemProps<E>) {
|
||||
const divRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
position={{x: 0, y: 0}}
|
||||
position={{ x: 0, y: 0 }}
|
||||
nodeRef={divRef}
|
||||
onStop={() => onTryDetach(divRef.current!, item)}>
|
||||
<div ref={divRef}>
|
||||
{render(item)}
|
||||
</div>
|
||||
<div ref={divRef}>{render(item)}</div>
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,32 @@
|
||||
import React, {CSSProperties, useRef, useState} from "react";
|
||||
import "../style/title_input.css";
|
||||
import React, { CSSProperties, useRef, useState } from "react"
|
||||
import "../style/title_input.css"
|
||||
|
||||
export interface TitleInputOptions {
|
||||
style: CSSProperties,
|
||||
default_value: string,
|
||||
style: CSSProperties
|
||||
default_value: string
|
||||
on_validated: (a: string) => void
|
||||
}
|
||||
|
||||
export default function TitleInput({style, default_value, on_validated}: TitleInputOptions) {
|
||||
const [value, setValue] = useState(default_value);
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
export default function TitleInput({
|
||||
style,
|
||||
default_value,
|
||||
on_validated,
|
||||
}: TitleInputOptions) {
|
||||
const [value, setValue] = useState(default_value)
|
||||
const ref = useRef<HTMLInputElement>(null)
|
||||
|
||||
return (
|
||||
<input className="title_input"
|
||||
ref={ref}
|
||||
style={style}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={event => setValue(event.target.value)}
|
||||
onBlur={_ => on_validated(value)}
|
||||
onKeyDown={event => {
|
||||
if (event.key == 'Enter')
|
||||
ref.current?.blur();
|
||||
}}
|
||||
<input
|
||||
className="title-input"
|
||||
ref={ref}
|
||||
style={style}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={(_) => on_validated(value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key == "Enter") ref.current?.blur()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,55 +1,77 @@
|
||||
import {useRef} from "react";
|
||||
import "../../style/player.css";
|
||||
import RemoveIcon from "../../assets/icon/remove.svg";
|
||||
import AssignBallIcon from "../../assets/icon/ball.svg";
|
||||
import Draggable, {DraggableBounds} from "react-draggable";
|
||||
import {PlayerPiece} from "./PlayerPiece";
|
||||
import { RefObject, useRef, useState } from "react"
|
||||
import "../../style/player.css"
|
||||
import RemoveIcon from "../../assets/icon/remove.svg?react"
|
||||
import BallIcon from "../../assets/icon/ball.svg?react"
|
||||
import Draggable from "react-draggable"
|
||||
import { PlayerPiece } from "./PlayerPiece"
|
||||
import { Player } from "../../tactic/Player"
|
||||
import { calculateRatio } from "../../Utils"
|
||||
|
||||
export interface PlayerProps {
|
||||
pos: string,
|
||||
team: string,
|
||||
x: number,
|
||||
y: number,
|
||||
bounds: DraggableBounds,
|
||||
onRemove: () => void,
|
||||
hasBall: boolean
|
||||
player: Player
|
||||
onChange: (p: Player) => void
|
||||
onRemove: () => void
|
||||
parentRef: RefObject<HTMLDivElement>
|
||||
}
|
||||
|
||||
/**
|
||||
* A player that is placed on the court, which can be selected, and moved in the associated bounds
|
||||
* */
|
||||
export default function CourtPlayer({pos, team, x, y, bounds, onRemove, hasBall}: PlayerProps) {
|
||||
export default function CourtPlayer({
|
||||
player,
|
||||
onChange,
|
||||
onRemove,
|
||||
assignBall,
|
||||
parentRef,
|
||||
}: PlayerProps) {
|
||||
const pieceRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const x = player.rightRatio
|
||||
const y = player.bottomRatio
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Draggable
|
||||
handle={".player-piece"}
|
||||
nodeRef={ref}
|
||||
bounds={bounds}
|
||||
defaultPosition={{x, y}}
|
||||
>
|
||||
<div ref={ref}
|
||||
className={"player"}
|
||||
style={{
|
||||
position: "absolute",
|
||||
}}>
|
||||
nodeRef={pieceRef}
|
||||
bounds="parent"
|
||||
position={{ x, y }}
|
||||
onStop={() => {
|
||||
const pieceBounds = pieceRef.current!.getBoundingClientRect()
|
||||
const parentBounds = parentRef.current!.getBoundingClientRect()
|
||||
|
||||
const { x, y } = calculateRatio(pieceBounds, parentBounds)
|
||||
|
||||
<div tabIndex={0}
|
||||
className="player-content"
|
||||
onKeyUp={e => {
|
||||
if (e.key == "Delete")
|
||||
onRemove()
|
||||
}}>
|
||||
onChange({
|
||||
rightRatio: x,
|
||||
bottomRatio: y,
|
||||
team: player.team,
|
||||
role: player.role,
|
||||
hasBall: false
|
||||
})
|
||||
}}>
|
||||
<div
|
||||
ref={pieceRef}
|
||||
className={"player"}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${x * 100}%`,
|
||||
top: `${y * 100}%`,
|
||||
}}>
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="player-content"
|
||||
onKeyUp={(e) => {
|
||||
if (e.key == "Delete") onRemove()
|
||||
}}>
|
||||
<div className="player-selection-tab">
|
||||
<RemoveIcon
|
||||
className="player-selection-tab-remove"
|
||||
onClick={() => onRemove()}/>
|
||||
onClick={onRemove}
|
||||
/>
|
||||
</div>
|
||||
<PlayerPiece team={team} text={pos}/>
|
||||
<PlayerPiece team={player.team} text={player.role} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Draggable>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import '../../style/player.css'
|
||||
import React from "react"
|
||||
import "../../style/player.css"
|
||||
import { Team } from "../../tactic/Team"
|
||||
|
||||
|
||||
export function PlayerPiece({team, text}: { team: string, text: string }) {
|
||||
export function PlayerPiece({ team, text }: { team: Team; text: string }) {
|
||||
return (
|
||||
<div className={`player-piece ${team}`}>
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
export interface SaveState {
|
||||
className: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export class SaveStates {
|
||||
static readonly Guest: SaveState = {
|
||||
className: "save-state-guest",
|
||||
message: "you are not connected, your changes will not be saved.",
|
||||
}
|
||||
static readonly Ok: SaveState = {
|
||||
className: "save-state-ok",
|
||||
message: "saved",
|
||||
}
|
||||
static readonly Saving: SaveState = {
|
||||
className: "save-state-saving",
|
||||
message: "saving...",
|
||||
}
|
||||
static readonly Err: SaveState = {
|
||||
className: "save-state-error",
|
||||
message: "could not save tactic.",
|
||||
}
|
||||
}
|
||||
|
||||
export default function SavingState({ state }: { state: SaveState }) {
|
||||
return (
|
||||
<div className={"save-state"}>
|
||||
<div className={state.className}>{state.message}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
#main {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#topbar {
|
||||
display: flex;
|
||||
background-color: var(--main-color);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#court-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: var(--main-color);
|
||||
}
|
||||
|
||||
#court {
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
export interface Ball {
|
||||
|
||||
position: string,
|
||||
|
||||
/**
|
||||
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
|
@ -1,32 +1,26 @@
|
||||
export interface Player {
|
||||
/**
|
||||
* unique identifier of the player.
|
||||
* This identifier must be unique to the associated court.
|
||||
*/
|
||||
id: number,
|
||||
import { Team } from "./Team"
|
||||
|
||||
export interface Player {
|
||||
/**
|
||||
* the player's team
|
||||
* */
|
||||
team: "allies" | "opponents",
|
||||
team: Team
|
||||
|
||||
/**
|
||||
* player's position
|
||||
* player's role
|
||||
* */
|
||||
position: string,
|
||||
role: string
|
||||
|
||||
/**
|
||||
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
|
||||
*/
|
||||
bottomRatio: number,
|
||||
|
||||
bottomRatio: number
|
||||
|
||||
/**
|
||||
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
|
||||
*/
|
||||
rightRatio: number,
|
||||
|
||||
rightRatio: number
|
||||
|
||||
hasBall: boolean,
|
||||
hasBall: boolean
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { Player } from "./Player"
|
||||
|
||||
export interface Tactic {
|
||||
id: number
|
||||
name: string
|
||||
content: TacticContent
|
||||
}
|
||||
|
||||
export interface TacticContent {
|
||||
players: Player[]
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export enum Team {
|
||||
Allies = "allies",
|
||||
Opponents = "opponents",
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
|
||||
interface DisplayResultsProps {
|
||||
results: readonly { name: string, description: string}[]
|
||||
}
|
||||
|
||||
export default function DisplayResults({results}: DisplayResultsProps) {
|
||||
const list = results
|
||||
.map(({name, description}) =>
|
||||
<div>
|
||||
<p>username: {name}</p>
|
||||
<p>description: {description}</p>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
{list}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
|
||||
|
||||
export default function SampleForm() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello, this is a sample form made in react !</h1>
|
||||
<form action="submit" method="POST">
|
||||
<label>your name: </label>
|
||||
<input type="text" id="name" name="name"/>
|
||||
<label>a little description about yourself: </label>
|
||||
<input type="text" id="password" name="description"/>
|
||||
<input type="submit" value="click me to submit!"/>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,23 @@
|
||||
import React, { CSSProperties, useState } from "react"
|
||||
import "../style/visualizer.css"
|
||||
import Court from "../assets/basketball_court.svg"
|
||||
|
||||
export default function Visualizer({ id, name }: { id: number; name: string }) {
|
||||
const [style, setStyle] = useState<CSSProperties>({})
|
||||
|
||||
return (
|
||||
<div id="main">
|
||||
<div id="topbar">
|
||||
<h1>{name}</h1>
|
||||
</div>
|
||||
<div id="court-container">
|
||||
<img
|
||||
id="court"
|
||||
src={Court}
|
||||
style={style}
|
||||
alt="Basketball Court"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
parameters:
|
||||
phpVersion: 70400
|
||||
level: 6
|
||||
paths:
|
||||
- src
|
||||
scanFiles:
|
||||
- config.php
|
||||
- sql/database.php
|
||||
- profiles/dev-config-profile.php
|
||||
- profiles/prod-config-profile.php
|
||||
excludePaths:
|
||||
- src/App/react-display-file.php
|
@ -0,0 +1 @@
|
||||
../front/assets
|
@ -1,76 +1,124 @@
|
||||
<?php
|
||||
|
||||
|
||||
require "../vendor/autoload.php";
|
||||
require "../config.php";
|
||||
require "../sql/database.php";
|
||||
require "utils.php";
|
||||
|
||||
use App\Connexion;
|
||||
use App\Controller\EditorController;
|
||||
use App\Controller\SampleFormController;
|
||||
use App\Gateway\FormResultGateway;
|
||||
use App\Gateway\TacticInfoGateway;
|
||||
use App\Http\JsonHttpResponse;
|
||||
use App\Http\ViewHttpResponse;
|
||||
use App\Model\TacticModel;
|
||||
require "../src/App/react-display.php";
|
||||
require "../src/index-utils.php";
|
||||
|
||||
use IQBall\App\App;
|
||||
use IQBall\App\Controller\AuthController;
|
||||
use IQBall\App\Controller\EditorController;
|
||||
use IQBall\App\Controller\TeamController;
|
||||
use IQBall\App\Controller\UserController;
|
||||
use IQBall\App\Controller\VisualizerController;
|
||||
use IQBall\App\Session\MutableSessionHandle;
|
||||
use IQBall\App\Session\PhpSessionHandle;
|
||||
use IQBall\App\Session\SessionHandle;
|
||||
use IQBall\App\ViewHttpResponse;
|
||||
use IQBall\Core\Action;
|
||||
use IQBall\Core\Connection;
|
||||
use IQBall\Core\Gateway\AccountGateway;
|
||||
use IQBall\Core\Gateway\MemberGateway;
|
||||
use IQBall\Core\Gateway\TacticInfoGateway;
|
||||
use IQBall\Core\Gateway\TeamGateway;
|
||||
use IQBall\Core\Http\HttpCodes;
|
||||
use IQBall\Core\Http\HttpResponse;
|
||||
use IQBall\Core\Model\AuthModel;
|
||||
use IQBall\Core\Model\TacticModel;
|
||||
use IQBall\Core\Model\TeamModel;
|
||||
use IQBall\Core\Validation\ValidationFail;
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use App\Validation\ValidationFail;
|
||||
use App\Controller\ErrorController;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
function getConnection(): Connection {
|
||||
return new Connection(get_database());
|
||||
}
|
||||
|
||||
$loader = new FilesystemLoader('../src/Views/');
|
||||
$twig = new \Twig\Environment($loader);
|
||||
function getUserController(): UserController {
|
||||
return new UserController(new TacticModel(new TacticInfoGateway(getConnection())));
|
||||
}
|
||||
|
||||
$basePath = get_public_path();
|
||||
$con = new Connexion(get_database());
|
||||
function getVisualizerController(): VisualizerController {
|
||||
return new VisualizerController(new TacticModel(new TacticInfoGateway(getConnection())));
|
||||
}
|
||||
|
||||
// routes initialization
|
||||
$router = new AltoRouter();
|
||||
$router->setBasePath($basePath);
|
||||
function getEditorController(): EditorController {
|
||||
return new EditorController(new TacticModel(new TacticInfoGateway(getConnection())));
|
||||
}
|
||||
|
||||
$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig);
|
||||
$editorController = new EditorController(new TacticModel(new TacticInfoGateway($con)));
|
||||
function getTeamController(): TeamController {
|
||||
$con = getConnection();
|
||||
return new TeamController(new TeamModel(new TeamGateway($con), new MemberGateway($con), new AccountGateway($con)));
|
||||
}
|
||||
|
||||
function getAuthController(): AuthController {
|
||||
return new AuthController(new AuthModel(new AccountGateway(getConnection())));
|
||||
}
|
||||
|
||||
$router->map("GET", "/", fn() => $sampleFormController->displayFormReact());
|
||||
$router->map("POST", "/submit", fn() => $sampleFormController->submitFormReact($_POST));
|
||||
$router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig());
|
||||
$router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST));
|
||||
$router->map("GET", "/tactic/new", fn() => $editorController->makeNew());
|
||||
$router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id));
|
||||
function getTwig(): Environment {
|
||||
global $basePath;
|
||||
$fl = new FilesystemLoader("../src/App/Views");
|
||||
$twig = new Environment($fl);
|
||||
|
||||
$match = $router->match();
|
||||
$twig->addFunction(new TwigFunction('path', fn(string $str) => "$basePath$str"));
|
||||
|
||||
if ($match == null) {
|
||||
http_response_code(404);
|
||||
ErrorController::displayFailures([ValidationFail::notFound("Cette page n'existe pas")], $twig);
|
||||
return;
|
||||
return $twig;
|
||||
}
|
||||
|
||||
$response = call_user_func_array($match['target'], $match['params']);
|
||||
|
||||
http_response_code($response->getCode());
|
||||
|
||||
if ($response instanceof ViewHttpResponse) {
|
||||
$file = $response->getFile();
|
||||
$args = $response->getArguments();
|
||||
|
||||
switch ($response->getViewKind()) {
|
||||
case ViewHttpResponse::REACT_VIEW:
|
||||
send_react_front($file, $args);
|
||||
break;
|
||||
case ViewHttpResponse::TWIG_VIEW:
|
||||
try {
|
||||
$twig->display($file, $args);
|
||||
} catch (\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) {
|
||||
http_response_code(500);
|
||||
echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s");
|
||||
throw e;
|
||||
}
|
||||
break;
|
||||
function getRoutes(): AltoRouter {
|
||||
global $basePath;
|
||||
|
||||
$ar = new AltoRouter();
|
||||
$ar->setBasePath($basePath);
|
||||
|
||||
//authentication
|
||||
$ar->map("GET", "/login", Action::noAuth(fn() => getAuthController()->displayLogin()));
|
||||
$ar->map("GET", "/register", Action::noAuth(fn() => getAuthController()->displayRegister()));
|
||||
$ar->map("POST", "/login", Action::noAuth(fn(SessionHandle $s) => getAuthController()->login($_POST, $s)));
|
||||
$ar->map("POST", "/register", Action::noAuth(fn(SessionHandle $s) => getAuthController()->register($_POST, $s)));
|
||||
|
||||
//user-related
|
||||
$ar->map("GET", "/", Action::auth(fn(SessionHandle $s) => getUserController()->home($s)));
|
||||
$ar->map("GET", "/home", Action::auth(fn(SessionHandle $s) => getUserController()->home($s)));
|
||||
$ar->map("GET", "/settings", Action::auth(fn(SessionHandle $s) => getUserController()->settings($s)));
|
||||
|
||||
//tactic-related
|
||||
$ar->map("GET", "/tactic/[i:id]/view", Action::auth(fn(int $id, SessionHandle $s) => getVisualizerController()->openVisualizer($id, $s)));
|
||||
$ar->map("GET", "/tactic/[i:id]/edit", Action::auth(fn(int $id, SessionHandle $s) => getEditorController()->openEditor($id, $s)));
|
||||
// don't require an authentication to run this action.
|
||||
// If the user is not connected, the tactic will never save.
|
||||
$ar->map("GET", "/tactic/new", Action::noAuth(fn(SessionHandle $s) => getEditorController()->createNew($s)));
|
||||
|
||||
//team-related
|
||||
$ar->map("GET", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->displayCreateTeam($s)));
|
||||
$ar->map("POST", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->submitTeam($_POST, $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("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)));
|
||||
|
||||
return $ar;
|
||||
}
|
||||
|
||||
function runMatch($match, MutableSessionHandle $session): HttpResponse {
|
||||
global $basePath;
|
||||
if (!$match) {
|
||||
return ViewHttpResponse::twig("error.html.twig", [
|
||||
'failures' => [ValidationFail::notFound("Could not find page {$_SERVER['REQUEST_URI']}.")],
|
||||
], HttpCodes::NOT_FOUND);
|
||||
}
|
||||
|
||||
} else if ($response instanceof JsonHttpResponse) {
|
||||
header('Content-type: application/json');
|
||||
echo $response->getJson();
|
||||
}
|
||||
return App::runAction($basePath . '/login', $match['target'], $match['params'], $session);
|
||||
}
|
||||
|
||||
|
||||
//this is a global variable
|
||||
$basePath = get_public_path(__DIR__);
|
||||
|
||||
App::render(runMatch(getRoutes()->match(), PhpSessionHandle::init()), fn() => getTwig());
|
||||
|
@ -1,12 +1,50 @@
|
||||
|
||||
-- drop tables here
|
||||
DROP TABLE IF EXISTS FormEntries;
|
||||
DROP TABLE IF EXISTS TacticInfo;
|
||||
DROP TABLE IF EXISTS Account;
|
||||
DROP TABLE IF EXISTS Tactic;
|
||||
DROP TABLE IF EXISTS Team;
|
||||
DROP TABLE IF EXISTS User;
|
||||
DROP TABLE IF EXISTS Member;
|
||||
CREATE TABLE Account
|
||||
(
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
email varchar UNIQUE NOT NULL,
|
||||
username varchar NOT NULL,
|
||||
token varchar UNIQUE NOT NULL,
|
||||
hash varchar NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE Tactic
|
||||
(
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
name varchar NOT NULL,
|
||||
creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
owner integer NOT NULL,
|
||||
content varchar DEFAULT '{"players": []}' NOT NULL,
|
||||
FOREIGN KEY (owner) REFERENCES Account
|
||||
);
|
||||
|
||||
CREATE TABLE FormEntries
|
||||
(
|
||||
name varchar,
|
||||
description varchar
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE Team
|
||||
(
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
name varchar,
|
||||
picture varchar,
|
||||
main_color varchar,
|
||||
second_color varchar
|
||||
);
|
||||
|
||||
CREATE TABLE FormEntries(name varchar, description varchar);
|
||||
|
||||
CREATE TABLE TacticInfo(
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
name varchar,
|
||||
creation_date timestamp DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE Member
|
||||
(
|
||||
id_team integer,
|
||||
id_user integer,
|
||||
role text CHECK (role IN ('Coach', 'Player')),
|
||||
FOREIGN KEY (id_team) REFERENCES Team (id),
|
||||
FOREIGN KEY (id_user) REFERENCES User (id)
|
||||
);
|
||||
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\Api;
|
||||
|
||||
use Exception;
|
||||
use IQBall\Core\Action;
|
||||
use IQBall\Core\Http\HttpResponse;
|
||||
use IQBall\Core\Http\JsonHttpResponse;
|
||||
use IQBall\Core\Validation\ValidationFail;
|
||||
|
||||
class API {
|
||||
public static function render(HttpResponse $response): void {
|
||||
http_response_code($response->getCode());
|
||||
|
||||
foreach ($response->getHeaders() as $header => $value) {
|
||||
header("$header: $value");
|
||||
}
|
||||
|
||||
if ($response instanceof JsonHttpResponse) {
|
||||
header('Content-type: application/json');
|
||||
echo $response->getJson();
|
||||
} elseif (get_class($response) != HttpResponse::class) {
|
||||
throw new Exception("API returned unknown Http Response");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $match
|
||||
* @param callable $tryGetAuthorization function to return an authorisation object for the given action (if required)
|
||||
* @return HttpResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function handleMatch(array $match, callable $tryGetAuthorization): HttpResponse {
|
||||
if (!$match) {
|
||||
return new JsonHttpResponse([ValidationFail::notFound("not found")]);
|
||||
}
|
||||
|
||||
$action = $match['target'];
|
||||
if (!$action instanceof Action) {
|
||||
throw new Exception("routed action is not an AppAction object.");
|
||||
}
|
||||
|
||||
$auth = null;
|
||||
|
||||
if ($action->isAuthRequired()) {
|
||||
$auth = call_user_func($tryGetAuthorization);
|
||||
if ($auth == null) {
|
||||
return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]);
|
||||
}
|
||||
}
|
||||
|
||||
return $action->run($match['params'], $auth);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\Api\Controller;
|
||||
|
||||
use IQBall\App\Control;
|
||||
use IQBall\Core\Http\HttpCodes;
|
||||
use IQBall\Core\Http\HttpRequest;
|
||||
use IQBall\Core\Http\HttpResponse;
|
||||
use IQBall\Core\Http\JsonHttpResponse;
|
||||
use IQBall\Core\Model\AuthModel;
|
||||
use IQBall\Core\Validation\Validators;
|
||||
|
||||
class APIAuthController {
|
||||
private AuthModel $model;
|
||||
|
||||
/**
|
||||
* @param AuthModel $model
|
||||
*/
|
||||
public function __construct(AuthModel $model) {
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* From given email address and password, authenticate the user and respond with its authorization token.
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public function authorize(): HttpResponse {
|
||||
return Control::runChecked([
|
||||
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
|
||||
"password" => [Validators::lenBetween(6, 256)],
|
||||
], function (HttpRequest $req) {
|
||||
$failures = [];
|
||||
$account = $this->model->login($req["email"], $req["password"], $failures);
|
||||
|
||||
if (!empty($failures)) {
|
||||
return new JsonHttpResponse($failures, HttpCodes::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
return new JsonHttpResponse(["authorization" => $account->getToken()]);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\Api\Controller;
|
||||
|
||||
use IQBall\App\Control;
|
||||
use IQBall\Core\Data\Account;
|
||||
use IQBall\Core\Http\HttpCodes;
|
||||
use IQBall\Core\Http\HttpRequest;
|
||||
use IQBall\Core\Http\HttpResponse;
|
||||
use IQBall\Core\Http\JsonHttpResponse;
|
||||
use IQBall\Core\Model\TacticModel;
|
||||
use IQBall\Core\Validation\FieldValidationFail;
|
||||
use IQBall\Core\Validation\Validators;
|
||||
|
||||
/**
|
||||
* API endpoint related to tactics
|
||||
*/
|
||||
class APITacticController {
|
||||
private TacticModel $model;
|
||||
|
||||
/**
|
||||
* @param TacticModel $model
|
||||
*/
|
||||
public function __construct(TacticModel $model) {
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* update name of tactic, specified by tactic identifier, given in url.
|
||||
* @param int $tactic_id
|
||||
* @param Account $account
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public function updateName(int $tactic_id, Account $account): HttpResponse {
|
||||
return Control::runChecked([
|
||||
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()],
|
||||
], function (HttpRequest $request) use ($tactic_id, $account) {
|
||||
|
||||
$failures = $this->model->updateName($tactic_id, $request["name"], $account->getId());
|
||||
|
||||
if (!empty($failures)) {
|
||||
//TODO find a system to handle Unauthorized error codes more easily from failures.
|
||||
return new JsonHttpResponse($failures, HttpCodes::BAD_REQUEST);
|
||||
}
|
||||
|
||||
return HttpResponse::fromCode(HttpCodes::OK);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @param Account $account
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public function saveContent(int $id, Account $account): HttpResponse {
|
||||
return Control::runChecked([
|
||||
"content" => [],
|
||||
], function (HttpRequest $req) use ($id) {
|
||||
if ($fail = $this->model->updateContent($id, json_encode($req["content"]))) {
|
||||
return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST);
|
||||
}
|
||||
return HttpResponse::fromCode(HttpCodes::OK);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\App;
|
||||
|
||||
use IQBall\App\Session\MutableSessionHandle;
|
||||
use IQBall\Core\Action;
|
||||
use IQBall\Core\Http\HttpResponse;
|
||||
use IQBall\Core\Http\JsonHttpResponse;
|
||||
use Twig\Environment;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
class App {
|
||||
/**
|
||||
* renders (prints out) given HttpResponse to the client
|
||||
* @param HttpResponse $response
|
||||
* @param callable(): Environment $twigSupplier
|
||||
* @return void
|
||||
* @throws LoaderError
|
||||
* @throws RuntimeError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
public static function render(HttpResponse $response, callable $twigSupplier): void {
|
||||
http_response_code($response->getCode());
|
||||
|
||||
foreach ($response->getHeaders() as $header => $value) {
|
||||
header("$header: $value");
|
||||
}
|
||||
|
||||
if ($response instanceof ViewHttpResponse) {
|
||||
self::renderView($response, $twigSupplier);
|
||||
} elseif ($response instanceof JsonHttpResponse) {
|
||||
header('Content-type: application/json');
|
||||
echo $response->getJson();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* renders (prints out) given ViewHttpResponse to the client
|
||||
* @param ViewHttpResponse $response
|
||||
* @param callable(): Environment $twigSupplier
|
||||
* @return void
|
||||
* @throws LoaderError
|
||||
* @throws RuntimeError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
private static function renderView(ViewHttpResponse $response, callable $twigSupplier): void {
|
||||
$file = $response->getFile();
|
||||
$args = $response->getArguments();
|
||||
|
||||
switch ($response->getViewKind()) {
|
||||
case ViewHttpResponse::REACT_VIEW:
|
||||
send_react_front($file, $args);
|
||||
break;
|
||||
case ViewHttpResponse::TWIG_VIEW:
|
||||
try {
|
||||
$twig = call_user_func($twigSupplier);
|
||||
$twig->display($file, $args);
|
||||
} catch (RuntimeError|SyntaxError|LoaderError $e) {
|
||||
http_response_code(500);
|
||||
echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s");
|
||||
throw $e;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* run a user action, and return the generated response
|
||||
* @param string $authRoute the route towards an authentication page to response with a redirection
|
||||
* if the run action requires auth but session does not contain a logged-in account.
|
||||
* @param Action<MutableSessionHandle> $action
|
||||
* @param mixed[] $params
|
||||
* @param MutableSessionHandle $session
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public static function runAction(string $authRoute, Action $action, array $params, MutableSessionHandle $session): HttpResponse {
|
||||
if ($action->isAuthRequired()) {
|
||||
$account = $session->getAccount();
|
||||
if ($account == null) {
|
||||
// put in the session the initial url the user wanted to get
|
||||
$session->setInitialTarget($_SERVER['REQUEST_URI']);
|
||||
return HttpResponse::redirect($authRoute);
|
||||
}
|
||||
}
|
||||
|
||||
return $action->run($params, $session);
|
||||
}
|
||||
|
||||
}
|
@ -1,61 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
namespace IQBall\App;
|
||||
|
||||
use App\Http\HttpCodes;
|
||||
use App\Http\HttpRequest;
|
||||
use App\Http\HttpResponse;
|
||||
use App\Http\JsonHttpResponse;
|
||||
use App\Http\ViewHttpResponse;
|
||||
use App\Validation\ValidationFail;
|
||||
use IQBall\Core\Http\HttpCodes;
|
||||
use IQBall\Core\Http\HttpRequest;
|
||||
use IQBall\Core\Http\HttpResponse;
|
||||
use IQBall\Core\Validation\ValidationFail;
|
||||
use IQBall\Core\Validation\Validator;
|
||||
|
||||
class Control {
|
||||
|
||||
/**
|
||||
* Runs given callback, if the request's json validates the given schema.
|
||||
* @param array $schema an array of `fieldName => Validators` which represents the request object schema
|
||||
* @param callable $run the callback to run if the request is valid according to the given schema.
|
||||
* @param array<string, Validator[]> $schema an array of `fieldName => Validators` which represents the request object schema
|
||||
* @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema.
|
||||
* THe callback must accept an HttpRequest, and return an HttpResponse object.
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public static function runChecked(array $schema, callable $run, bool $errorInJson): HttpResponse {
|
||||
public static function runChecked(array $schema, callable $run): HttpResponse {
|
||||
$request_body = file_get_contents('php://input');
|
||||
$payload_obj = json_decode($request_body);
|
||||
if (!$payload_obj instanceof \stdClass) {
|
||||
$fail = new ValidationFail("bad-payload", "request body is not a valid json object");
|
||||
if($errorInJson) {
|
||||
return new JsonHttpResponse([$fail, HttpCodes::BAD_REQUEST]);
|
||||
}
|
||||
return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST);
|
||||
}
|
||||
$payload = get_object_vars($payload_obj);
|
||||
return self::runCheckedFrom($payload, $schema, $run, $errorInJson);
|
||||
return self::runCheckedFrom($payload, $schema, $run);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs given callback, if the given request data array validates the given schema.
|
||||
* @param array $data the request's data array.
|
||||
* @param array $schema an array of `fieldName => Validators` which represents the request object schema
|
||||
* @param callable $run the callback to run if the request is valid according to the given schema.
|
||||
* @param array<string, mixed> $data the request's data array.
|
||||
* @param array<string, Validator[]> $schema an array of `fieldName => Validators` which represents the request object schema
|
||||
* @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema.
|
||||
* THe callback must accept an HttpRequest, and return an HttpResponse object.
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public static function runCheckedFrom(array $data, array $schema, callable $run, bool $errorInJson): HttpResponse {
|
||||
public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse {
|
||||
$fails = [];
|
||||
$request = HttpRequest::from($data, $fails, $schema);
|
||||
|
||||
if (!empty($fails)) {
|
||||
if($errorInJson) {
|
||||
return new JsonHttpResponse($fails, HttpCodes::BAD_REQUEST);
|
||||
}
|
||||
return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST);
|
||||
}
|
||||
|
||||
return call_user_func_array($run, [$request]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\App\Controller;
|
||||
|
||||
use IQBall\App\Session\MutableSessionHandle;
|
||||
use IQBall\App\ViewHttpResponse;
|
||||
use IQBall\Core\Http\HttpRequest;
|
||||
use IQBall\Core\Http\HttpResponse;
|
||||
use IQBall\Core\Model\AuthModel;
|
||||
use IQBall\Core\Validation\Validators;
|
||||
|
||||
class AuthController {
|
||||
private AuthModel $model;
|
||||
|
||||
/**
|
||||
* @param AuthModel $model
|
||||
*/
|
||||
public function __construct(AuthModel $model) {
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
public function displayRegister(): HttpResponse {
|
||||
return ViewHttpResponse::twig("display_register.html.twig", []);
|
||||
}
|
||||
|
||||
/**
|
||||
* registers given account
|
||||
* @param mixed[] $request
|
||||
* @param MutableSessionHandle $session
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public function register(array $request, MutableSessionHandle $session): HttpResponse {
|
||||
$fails = [];
|
||||
$request = HttpRequest::from($request, $fails, [
|
||||
"username" => [Validators::name(), Validators::lenBetween(2, 32)],
|
||||
"password" => [Validators::lenBetween(6, 256)],
|
||||
"confirmpassword" => [Validators::lenBetween(6, 256)],
|
||||
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
|
||||
]);
|
||||
if (!empty($fails)) {
|
||||
return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]);
|
||||
}
|
||||
$account = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email'], $fails);
|
||||
if (!empty($fails)) {
|
||||
return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]);
|
||||
}
|
||||
|
||||
$session->setAccount($account);
|
||||
|
||||
$target_url = $session->getInitialTarget();
|
||||
return HttpResponse::redirect($target_url ?? "/home");
|
||||
}
|
||||
|
||||
|
||||
public function displayLogin(): HttpResponse {
|
||||
return ViewHttpResponse::twig("display_login.html.twig", []);
|
||||
}
|
||||
|
||||
/**
|
||||
* logins given account credentials
|
||||
* @param mixed[] $request
|
||||
* @param MutableSessionHandle $session
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public function login(array $request, MutableSessionHandle $session): HttpResponse {
|
||||
$fails = [];
|
||||
$request = HttpRequest::from($request, $fails, [
|
||||
"password" => [Validators::lenBetween(6, 256)],
|
||||
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
|
||||
]);
|
||||
if (!empty($fails)) {
|
||||
return ViewHttpResponse::twig("display_login.html.twig", ['fails' => $fails]);
|
||||
}
|
||||
|
||||
$account = $this->model->login($request['email'], $request['password'], $fails);
|
||||
if (!empty($fails)) {
|
||||
return ViewHttpResponse::twig("display_login.html.twig", ['fails' => $fails]);
|
||||
}
|
||||
|
||||
$session->setAccount($account);
|
||||
|
||||
$target_url = $session->getInitialTarget();
|
||||
$session->setInitialTarget(null);
|
||||
return HttpResponse::redirect($target_url ?? "/home");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\App\Controller;
|
||||
|
||||
use IQBall\App\Session\SessionHandle;
|
||||
use IQBall\App\Validator\TacticValidator;
|
||||
use IQBall\App\ViewHttpResponse;
|
||||
use IQBall\Core\Data\TacticInfo;
|
||||
use IQBall\Core\Http\HttpCodes;
|
||||
use IQBall\Core\Model\TacticModel;
|
||||
|
||||
class EditorController {
|
||||
private TacticModel $model;
|
||||
|
||||
public function __construct(TacticModel $model) {
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TacticInfo $tactic
|
||||
* @return ViewHttpResponse the editor view for given tactic
|
||||
*/
|
||||
private function openEditorFor(TacticInfo $tactic): ViewHttpResponse {
|
||||
return ViewHttpResponse::react("views/Editor.tsx", [
|
||||
"id" => $tactic->getId(),
|
||||
"name" => $tactic->getName(),
|
||||
"content" => $tactic->getContent(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ViewHttpResponse the editor view for a test tactic.
|
||||
*/
|
||||
private function openTestEditor(): ViewHttpResponse {
|
||||
return ViewHttpResponse::react("views/Editor.tsx", [
|
||||
"id" => -1, //-1 id means that the editor will not support saves
|
||||
"content" => '{"players": []}',
|
||||
"name" => TacticModel::TACTIC_DEFAULT_NAME,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a new empty tactic, with default name
|
||||
* If the given session does not contain a connected account,
|
||||
* open a test editor.
|
||||
* @param SessionHandle $session
|
||||
* @return ViewHttpResponse the editor view
|
||||
*/
|
||||
public function createNew(SessionHandle $session): ViewHttpResponse {
|
||||
$account = $session->getAccount();
|
||||
|
||||
if ($account == null) {
|
||||
return $this->openTestEditor();
|
||||
}
|
||||
$tactic = $this->model->makeNewDefault($account->getId());
|
||||
return $this->openEditorFor($tactic);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an editor view for a given tactic
|
||||
* @param int $id the targeted tactic identifier
|
||||
* @param SessionHandle $session
|
||||
* @return ViewHttpResponse
|
||||
*/
|
||||
public function openEditor(int $id, SessionHandle $session): ViewHttpResponse {
|
||||
$tactic = $this->model->get($id);
|
||||
|
||||
$failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getId());
|
||||
|
||||
if ($failure != null) {
|
||||
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);
|
||||
}
|
||||
|
||||
return $this->openEditorFor($tactic);
|
||||
}
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\App\Controller;
|
||||
|
||||
use IQBall\App\Session\SessionHandle;
|
||||
use IQBall\App\ViewHttpResponse;
|
||||
use IQBall\Core\Http\HttpRequest;
|
||||
use IQBall\Core\Http\HttpResponse;
|
||||
use IQBall\Core\Model\TeamModel;
|
||||
use IQBall\Core\Validation\FieldValidationFail;
|
||||
use IQBall\Core\Validation\Validators;
|
||||
|
||||
class TeamController {
|
||||
private TeamModel $model;
|
||||
|
||||
/**
|
||||
* @param TeamModel $model
|
||||
*/
|
||||
public function __construct(TeamModel $model) {
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SessionHandle $session
|
||||
* @return ViewHttpResponse the team creation panel
|
||||
*/
|
||||
public function displayCreateTeam(SessionHandle $session): ViewHttpResponse {
|
||||
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
|
||||
*/
|
||||
public function displayDeleteMember(SessionHandle $session): ViewHttpResponse {
|
||||
return ViewHttpResponse::twig("delete_member.html.twig", []);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a new team from given request name, mainColor, secondColor and picture url
|
||||
* @param array<string, mixed> $request
|
||||
* @param SessionHandle $session
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public function submitTeam(array $request, SessionHandle $session): HttpResponse {
|
||||
$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)) {
|
||||
$badFields = [];
|
||||
foreach ($failures as $e) {
|
||||
if ($e instanceof FieldValidationFail) {
|
||||
$badFields[] = $e->getFieldName();
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SessionHandle $session
|
||||
* @return ViewHttpResponse the panel to search a team by its name
|
||||
*/
|
||||
public function displayListTeamByName(SessionHandle $session): ViewHttpResponse {
|
||||
return ViewHttpResponse::twig("list_team_by_name.html.twig", []);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a view that contains all the teams description whose name matches the given name needle.
|
||||
* @param array<string, mixed> $request
|
||||
* @param SessionHandle $session
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public function listTeamByName(array $request, SessionHandle $session): HttpResponse {
|
||||
$errors = [];
|
||||
$request = HttpRequest::from($request, $errors, [
|
||||
"name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()],
|
||||
]);
|
||||
|
||||
if (!empty($errors) && $errors[0] instanceof FieldValidationFail) {
|
||||
$badField = $errors[0]->getFieldName();
|
||||
return ViewHttpResponse::twig('list_team_by_name.html.twig', ['bad_field' => $badField]);
|
||||
}
|
||||
|
||||
$teams = $this->model->listByName($request['name']);
|
||||
|
||||
if (empty($teams)) {
|
||||
return ViewHttpResponse::twig('display_teams.html.twig', []);
|
||||
}
|
||||
|
||||
return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $teams]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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]);
|
||||
}
|
||||
|
||||
/**
|
||||
* add a member to a team
|
||||
* @param array<string, mixed> $request
|
||||
* @param SessionHandle $session
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public function addMember(array $request, SessionHandle $session): HttpResponse {
|
||||
$errors = [];
|
||||
|
||||
$request = HttpRequest::from($request, $errors, [
|
||||
"team" => [Validators::isInteger()],
|
||||
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
|
||||
]);
|
||||
|
||||
$teamId = intval($request['team']);
|
||||
$this->model->addMember($request['email'], $teamId, $request['role']);
|
||||
return $this->displayTeam($teamId, $session);
|
||||
}
|
||||
|
||||
/**
|
||||
* remove a member from a team
|
||||
* @param array<string, mixed> $request
|
||||
* @param SessionHandle $session
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public function deleteMember(array $request, SessionHandle $session): HttpResponse {
|
||||
$errors = [];
|
||||
|
||||
$request = HttpRequest::from($request, $errors, [
|
||||
"team" => [Validators::isInteger()],
|
||||
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
|
||||
]);
|
||||
|
||||
return $this->displayTeam($this->model->deleteMember($request['email'], intval($request['team'])), $session);
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\App\Controller;
|
||||
|
||||
use IQBall\App\Session\SessionHandle;
|
||||
use IQBall\App\ViewHttpResponse;
|
||||
use IQBall\Core\Model\TacticModel;
|
||||
|
||||
class UserController {
|
||||
private TacticModel $tactics;
|
||||
|
||||
/**
|
||||
* @param TacticModel $tactics
|
||||
*/
|
||||
public function __construct(TacticModel $tactics) {
|
||||
$this->tactics = $tactics;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SessionHandle $session
|
||||
* @return ViewHttpResponse the home page view
|
||||
*/
|
||||
public function home(SessionHandle $session): ViewHttpResponse {
|
||||
//TODO use session's account to get the last 5 tactics of the logged-in account
|
||||
$listTactic = $this->tactics->getLast(5);
|
||||
return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ViewHttpResponse account settings page
|
||||
*/
|
||||
public function settings(SessionHandle $session): ViewHttpResponse {
|
||||
return ViewHttpResponse::twig("account_settings.twig", []);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\App\Controller;
|
||||
|
||||
use IQBall\App\Session\SessionHandle;
|
||||
use IQBall\App\Validator\TacticValidator;
|
||||
use IQBall\App\ViewHttpResponse;
|
||||
use IQBall\Core\Http\HttpCodes;
|
||||
use IQBall\Core\Http\HttpResponse;
|
||||
use IQBall\Core\Model\TacticModel;
|
||||
|
||||
class VisualizerController {
|
||||
private TacticModel $tacticModel;
|
||||
|
||||
/**
|
||||
* @param TacticModel $tacticModel
|
||||
*/
|
||||
public function __construct(TacticModel $tacticModel) {
|
||||
$this->tacticModel = $tacticModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a visualisation page for the tactic specified by its identifier in the url.
|
||||
* @param int $id
|
||||
* @param SessionHandle $session
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public function openVisualizer(int $id, SessionHandle $session): HttpResponse {
|
||||
$tactic = $this->tacticModel->get($id);
|
||||
|
||||
$failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getId());
|
||||
|
||||
if ($failure != null) {
|
||||
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);
|
||||
}
|
||||
|
||||
return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\App\Session;
|
||||
|
||||
use IQBall\Core\Data\Account;
|
||||
|
||||
/**
|
||||
* The mutable side of a session handle
|
||||
*/
|
||||
interface MutableSessionHandle extends SessionHandle {
|
||||
/**
|
||||
* @param string|null $url the url to redirect the user to after authentication.
|
||||
*/
|
||||
public function setInitialTarget(?string $url): void;
|
||||
|
||||
/**
|
||||
* @param Account $account update the session's account
|
||||
*/
|
||||
public function setAccount(Account $account): void;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\App\Session;
|
||||
|
||||
use IQBall\Core\Data\Account;
|
||||
|
||||
/**
|
||||
* A PHP session handle
|
||||
*/
|
||||
class PhpSessionHandle implements MutableSessionHandle {
|
||||
public static function init(): self {
|
||||
if (session_status() !== PHP_SESSION_NONE) {
|
||||
throw new \Exception("A php session is already started !");
|
||||
}
|
||||
session_start();
|
||||
return new PhpSessionHandle();
|
||||
}
|
||||
|
||||
public function getAccount(): ?Account {
|
||||
return $_SESSION["account"] ?? null;
|
||||
}
|
||||
|
||||
public function getInitialTarget(): ?string {
|
||||
return $_SESSION["target"] ?? null;
|
||||
}
|
||||
|
||||
public function setAccount(Account $account): void {
|
||||
$_SESSION["account"] = $account;
|
||||
}
|
||||
|
||||
public function setInitialTarget(?string $url): void {
|
||||
$_SESSION["target"] = $url;
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\App\Session;
|
||||
|
||||
use IQBall\Core\Data\Account;
|
||||
|
||||
/**
|
||||
* An immutable session handle
|
||||
*/
|
||||
interface SessionHandle {
|
||||
/**
|
||||
* The initial target url if the user wanted to perform an action that requires authentication
|
||||
* but has been required to login first in the application.
|
||||
* @return string|null Get the initial targeted URL
|
||||
*/
|
||||
public function getInitialTarget(): ?string;
|
||||
|
||||
/**
|
||||
* The session account if the user is logged in.
|
||||
* @return Account|null
|
||||
*/
|
||||
public function getAccount(): ?Account;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\App\Validator;
|
||||
|
||||
use IQBall\Core\Data\TacticInfo;
|
||||
use IQBall\Core\Validation\ValidationFail;
|
||||
|
||||
class TacticValidator {
|
||||
public static function validateAccess(int $tacticId, ?TacticInfo $tactic, int $ownerId): ?ValidationFail {
|
||||
if ($tactic == null) {
|
||||
return ValidationFail::notFound("La tactique $tacticId n'existe pas");
|
||||
}
|
||||
if ($tactic->getOwnerId() != $ownerId) {
|
||||
return ValidationFail::unauthorized("Vous ne pouvez pas accéder à cette tactique.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Paramètres</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
||||
<body>
|
||||
<button onclick="location.pathname='{{ path('/home') }}'">Retour</button>
|
||||
<h1>Paramètres</h1>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ajouter un membre</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 400px;
|
||||
margin: 0 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;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="radio"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.role {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.radio {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<h2>Ajouter un membre à votre équipe</h2>
|
||||
<form action="{{ path('/team/members/add') }}" 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>
|
||||
|
||||
<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 />
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label for="C">Coach</label>
|
||||
<input type="radio" id="C" name="role" value="C" />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Confirmer">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ajouter un membre</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 400px;
|
||||
margin: 0 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;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
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>Supprimez un membre de votre équipe</h2>
|
||||
<form action="{{ path('/team/members/remove') }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="team">Team où supprimer 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>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Confirmer">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,46 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Profil Utilisateur</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.user-profile {
|
||||
background-color: #7FBFFF;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="user-profile">
|
||||
<h1>Votre profil</h1>
|
||||
<p><strong>Pseudo : </strong> {{ username }} </p>
|
||||
<p><strong>Email : {{ email }} </strong></p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,96 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Connexion</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 400px;
|
||||
margin: 0 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.error-messages {
|
||||
color: #ff331a;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
{% for err in fails %}
|
||||
.form-group
|
||||
|
||||
#
|
||||
{{ err.getFieldName() }}
|
||||
{
|
||||
border-color: red
|
||||
;
|
||||
}
|
||||
{% endfor %}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<center><h2>Se connecter</h2></center>
|
||||
<form action="{{ path('/login') }}" method="post">
|
||||
<div class="form-group">
|
||||
|
||||
{% for name in fails %}
|
||||
<label class="error-messages"> {{ name.getFieldName() }} : {{ name.getMessage() }} </label>
|
||||
{% endfor %}
|
||||
|
||||
<label for="email">Email :</label>
|
||||
<input type="text" id="email" name="email" required>
|
||||
<label for="password">Mot de passe :</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="S'identifier">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,102 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>S'enregistrer</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 400px;
|
||||
margin: 0 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.error-messages {
|
||||
color: #ff331a;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
input[type="submit"]:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
{% for err in fails %}
|
||||
.form-group
|
||||
|
||||
#
|
||||
{{ err.getFieldName() }}
|
||||
{
|
||||
border-color: red
|
||||
;
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<center><h2>S'enregistrer</h2></center>
|
||||
<form action="{{ path('/register') }}" method="post">
|
||||
<div class="form-group">
|
||||
|
||||
{% for name in fails %}
|
||||
<label class="error-messages"> {{ name.getFieldName() }} : {{ name.getMessage() }} </label>
|
||||
{% endfor %}
|
||||
|
||||
<label for="username">Nom d'utilisateur :</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
<label for="password">Mot de passe :</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<label for="confirmpassword">Confirmer le mot de passe :</label>
|
||||
<input type="password" id="confirmpassword" name="confirmpassword" required>
|
||||
|
||||
<label for="email">Email :</label>
|
||||
<input type="text" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Confirmer">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Twig view</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #f1f1f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
section {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.square {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
#main_color {
|
||||
border: solid;
|
||||
background-color: {{ team.getInfo().getMainColor().getValue() }};
|
||||
}
|
||||
|
||||
#second_color {
|
||||
background-color: {{ team.getInfo().getSecondColor().getValue() }};
|
||||
border: solid;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.team {
|
||||
border-color: darkgrey;
|
||||
border-radius: 20px;
|
||||
|
||||
}
|
||||
|
||||
.color {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="{{ path('/') }}">IQBall</a></h1>
|
||||
</header>
|
||||
|
||||
<section class="container">
|
||||
|
||||
<div class="team container">
|
||||
<div>
|
||||
<h1>{{ team.getInfo().getName() }}</h1>
|
||||
<img src="{{ team.getInfo().getPicture() }}" alt="Logo d'équipe" class="logo">
|
||||
</div>
|
||||
<div>
|
||||
<div class="color"><p>Couleur principale : </p>
|
||||
<div class="square" id="main_color"></div>
|
||||
</div>
|
||||
<div class="color"><p>Couleur secondaire : </p>
|
||||
<div class="square" id="second_color"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for m in team.listMembers() %}
|
||||
<p> {{ m.getUserId() }} </p>
|
||||
{% if m.getRole().isCoach() %}
|
||||
<p> : Coach</p>
|
||||
{% else %}
|
||||
<p> : Joueur</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Twig view</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% if teams is empty %}
|
||||
<p>Aucune équipe n'a été trouvée</p>
|
||||
<div class="container">
|
||||
<h2>Chercher une équipe</h2>
|
||||
<form action="{{ path('/team/search') }}" method="post">
|
||||
<div class="form-group">
|
||||
<label for="name">Nom de l'équipe :</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Confirmer">
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Error</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #da6110;
|
||||
text-align: center;
|
||||
margin-bottom: 15px
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
margin-top: 15px
|
||||
}
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
color: black;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #da6110;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #da6110
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>IQBall</h1>
|
||||
|
||||
{% for fail in failures %}
|
||||
<h2>{{ fail.getKind() }} : {{ fail.getMessage() }}</h2>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
<button class="button" onclick="location.href='{{ path('/home') }}'" type="button">Retour à la page d'accueil</button>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,96 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Page d'accueil</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
|
||||
#bandeau {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#bandeau > h1 {
|
||||
self-align: center;
|
||||
padding: 0%;
|
||||
margin: 0%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#account {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
#account:hover {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
#account img {
|
||||
width: 70%;
|
||||
height: auto;
|
||||
align-self: center;
|
||||
padding: 5%;
|
||||
margin: 0%;
|
||||
}
|
||||
|
||||
#account p {
|
||||
align-self: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="bandeau">
|
||||
<h1>IQ Ball</h1>
|
||||
<div id="account" onclick="location.pathname='{{ path('/settings') }}'">
|
||||
<img
|
||||
src="{{ path('/assets/icon/account.svg') }}"
|
||||
alt="Account logo"
|
||||
/>
|
||||
<p>Mon profil
|
||||
<p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Mes équipes</h2>
|
||||
|
||||
<button onclick="location.pathname='{{ path('/team/new') }}'"> Créer une nouvelle équipe</button>
|
||||
|
||||
{% if recentTeam != null %}
|
||||
{% for team in recentTeam %}
|
||||
<div>
|
||||
<p> {{ team.name }} </p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>Aucune équipe créé !</p>
|
||||
{% endif %}
|
||||
|
||||
<h2> Mes strategies </h2>
|
||||
|
||||
<button onclick="location.pathname='{{ path('/tactic/new') }}'"> Créer une nouvelle tactique</button>
|
||||
|
||||
{% if recentTactic != null %}
|
||||
{% for tactic in recentTactic %}
|
||||
<div onclick="location.pathname='{{ path("/tactic/#{strategie.id}/edit") }}'">
|
||||
<p> {{ tactic.id }} - {{ tactic.name }} - {{ tactic.creation_date }} </p>
|
||||
<button onclick="location.pathname='{{ path("/tactic/#{tactic.id}/edit") }}'"> Editer la
|
||||
stratégie {{ tactic.id }} </button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p> Aucune tactique créé !</p>
|
||||
{% endif %}
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,82 @@
|
||||
<!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: 0 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>Créer une équipe</h2>
|
||||
<form action="{{ path('/team/new') }}" method="post">
|
||||
<div class="form-group">
|
||||
<label for="name">Nom de l'équipe :</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
<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>
|
||||
<label for="second_color">Couleur secondaire</label>
|
||||
<input type="color" id="second_color" name="second_color" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Confirmer">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,74 @@
|
||||
<!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: 0 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>Chercher une équipe</h2>
|
||||
<form action="{{ path('/team/search') }}" method="post">
|
||||
<div class="form-group">
|
||||
<label for="name">Nom de l'équipe :</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Confirmer">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
use \PDO;
|
||||
|
||||
class Connexion {
|
||||
|
||||
private PDO $pdo;
|
||||
|
||||
/**
|
||||
* @param PDO $pdo
|
||||
*/
|
||||
public function __construct(PDO $pdo)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
public function lastInsertId() {
|
||||
return $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* execute a request
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return void
|
||||
*/
|
||||
public function exec(string $query, array $args) {
|
||||
$stmnt = $this->prepare($query, $args);
|
||||
$stmnt->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a request, and return the returned rows
|
||||
* @param string $query the SQL request
|
||||
* @param array $args an array containing the arguments label, value and type: ex: `[":label" => [$value, PDO::PARAM_TYPE]`
|
||||
* @return array the returned rows of the request
|
||||
*/
|
||||
public function fetch(string $query, array $args): array {
|
||||
$stmnt = $this->prepare($query, $args);
|
||||
$stmnt->execute();
|
||||
return $stmnt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
private function prepare(string $query, array $args): \PDOStatement {
|
||||
$stmnt = $this->pdo->prepare($query);
|
||||
foreach ($args as $name => $value) {
|
||||
$stmnt->bindValue($name, $value[0], $value[1]);
|
||||
}
|
||||
return $stmnt;
|
||||
}
|
||||
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\Controller\Control;
|
||||
use App\Http\HttpCodes;
|
||||
use App\Http\HttpRequest;
|
||||
use App\Http\HttpResponse;
|
||||
use App\Http\JsonHttpResponse;
|
||||
use App\Model\TacticModel;
|
||||
use App\Validation\Validators;
|
||||
|
||||
/**
|
||||
* API endpoint related to tactics
|
||||
*/
|
||||
class APITacticController {
|
||||
private TacticModel $model;
|
||||
|
||||
/**
|
||||
* @param TacticModel $model
|
||||
*/
|
||||
public function __construct(TacticModel $model) {
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
public function updateName(int $tactic_id): HttpResponse {
|
||||
return Control::runChecked([
|
||||
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()]
|
||||
], function (HttpRequest $request) use ($tactic_id) {
|
||||
$this->model->updateName($tactic_id, $request["name"]);
|
||||
return HttpResponse::fromCode(HttpCodes::OK);
|
||||
}, true);
|
||||
}
|
||||
|
||||
public function newTactic(): HttpResponse {
|
||||
return Control::runChecked([
|
||||
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()]
|
||||
], function (HttpRequest $request) {
|
||||
$tactic = $this->model->makeNew($request["name"]);
|
||||
$id = $tactic->getId();
|
||||
return new JsonHttpResponse(["id" => $id]);
|
||||
}, true);
|
||||
}
|
||||
|
||||
public function getTacticInfo(int $id): HttpResponse {
|
||||
$tactic_info = $this->model->get($id);
|
||||
|
||||
if ($tactic_info == null) {
|
||||
return new JsonHttpResponse("could not find tactic #$id", HttpCodes::NOT_FOUND);
|
||||
}
|
||||
|
||||
return new JsonHttpResponse($tactic_info);
|
||||
}
|
||||
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Data\TacticInfo;
|
||||
use App\Http\HttpCodes;
|
||||
use App\Http\HttpRequest;
|
||||
use App\Http\HttpResponse;
|
||||
use App\Http\JsonHttpResponse;
|
||||
use App\Http\ViewHttpResponse;
|
||||
use App\Model\TacticModel;
|
||||
|
||||
class EditorController {
|
||||
|
||||
private TacticModel $model;
|
||||
|
||||
/**
|
||||
* @param TacticModel $model
|
||||
*/
|
||||
public function __construct(TacticModel $model) {
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
private function openEditor(TacticInfo $tactic): HttpResponse {
|
||||
return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]);
|
||||
}
|
||||
|
||||
public function makeNew(): HttpResponse {
|
||||
$tactic = $this->model->makeNewDefault();
|
||||
return $this->openEditor($tactic);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an editor view for a given tactic
|
||||
* @param int $id the targeted tactic identifier
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public function openEditorFor(int $id): HttpResponse {
|
||||
$tactic = $this->model->get($id);
|
||||
|
||||
if ($tactic == null) {
|
||||
return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND);
|
||||
}
|
||||
|
||||
return $this->openEditor($tactic);
|
||||
}
|
||||
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
require_once __DIR__ . "/../react-display.php";
|
||||
use \Twig\Environment;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
|
||||
class ErrorController
|
||||
{
|
||||
public static function displayFailures(array $failures, Environment $twig) {
|
||||
try {
|
||||
$twig->display("error.html.twig", ['failures' => $failures]);
|
||||
} catch (LoaderError | RuntimeError | SyntaxError $e) {
|
||||
echo "Twig error: $e";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
require_once __DIR__ . "/../react-display.php";
|
||||
|
||||
use App\Gateway\FormResultGateway;
|
||||
use App\Http\HttpRequest;
|
||||
use App\Http\HttpResponse;
|
||||
use App\Http\ViewHttpResponse;
|
||||
use App\Validation\Validators;
|
||||
|
||||
class SampleFormController {
|
||||
|
||||
private FormResultGateway $gateway;
|
||||
|
||||
/**
|
||||
* @param FormResultGateway $gateway
|
||||
*/
|
||||
public function __construct(FormResultGateway $gateway) {
|
||||
$this->gateway = $gateway;
|
||||
}
|
||||
|
||||
|
||||
public function displayFormReact(): HttpResponse {
|
||||
return ViewHttpResponse::react("views/SampleForm.tsx", []);
|
||||
}
|
||||
|
||||
public function displayFormTwig(): HttpResponse {
|
||||
return ViewHttpResponse::twig('sample_form.html.twig', []);
|
||||
}
|
||||
|
||||
private function submitForm(array $form, callable $response): HttpResponse {
|
||||
return Control::runCheckedFrom($form, [
|
||||
"name" => [Validators::lenBetween(0, 32), Validators::name("Le nom ne peut contenir que des lettres, des chiffres et des accents")],
|
||||
"description" => [Validators::lenBetween(0, 512)]
|
||||
], function (HttpRequest $req) use ($response) {
|
||||
$description = htmlspecialchars($req["description"]);
|
||||
$this->gateway->insert($req["name"], $description);
|
||||
$results = ["results" => $this->gateway->listResults()];
|
||||
return call_user_func_array($response, [$results]);
|
||||
}, false);
|
||||
}
|
||||
|
||||
public function submitFormTwig(array $form): HttpResponse {
|
||||
return $this->submitForm($form, fn(array $results) => ViewHttpResponse::twig('display_results.html.twig', $results));
|
||||
}
|
||||
|
||||
public function submitFormReact(array $form): HttpResponse {
|
||||
return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results));
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\Core;
|
||||
|
||||
use IQBall\Core\Http\HttpResponse;
|
||||
|
||||
/**
|
||||
* Represent an action.
|
||||
* @template S session
|
||||
*/
|
||||
class Action {
|
||||
/**
|
||||
* @var callable(mixed[], S): HttpResponse $action action to call
|
||||
*/
|
||||
protected $action;
|
||||
|
||||
private bool $isAuthRequired;
|
||||
|
||||
/**
|
||||
* @param callable(mixed[], S): HttpResponse $action
|
||||
*/
|
||||
protected function __construct(callable $action, bool $isAuthRequired) {
|
||||
$this->action = $action;
|
||||
$this->isAuthRequired = $isAuthRequired;
|
||||
}
|
||||
|
||||
public function isAuthRequired(): bool {
|
||||
return $this->isAuthRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an action
|
||||
* @param mixed[] $params
|
||||
* @param S $session
|
||||
* @return HttpResponse
|
||||
*/
|
||||
public function run(array $params, $session): HttpResponse {
|
||||
$params = array_values($params);
|
||||
$params[] = $session;
|
||||
return call_user_func_array($this->action, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(mixed[], S): HttpResponse $action
|
||||
* @return Action<S> an action that does not require to have an authorization.
|
||||
*/
|
||||
public static function noAuth(callable $action): Action {
|
||||
return new Action($action, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(mixed[], S): HttpResponse $action
|
||||
* @return Action<S> an action that does require to have an authorization.
|
||||
*/
|
||||
public static function auth(callable $action): Action {
|
||||
return new Action($action, true);
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\Core;
|
||||
|
||||
use PDO;
|
||||
|
||||
class Connection {
|
||||
private PDO $pdo;
|
||||
|
||||
/**
|
||||
* @param PDO $pdo
|
||||
*/
|
||||
public function __construct(PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
public function lastInsertId(): string {
|
||||
return $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* execute a request
|
||||
* @param string $query
|
||||
* @param array<string, array<mixed, int>> $args
|
||||
* @return void
|
||||
*/
|
||||
public function exec(string $query, array $args) {
|
||||
$stmnt = $this->prep($query, $args);
|
||||
$stmnt->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a request, and return the returned rows
|
||||
* @param string $query the SQL request
|
||||
* @param array<string, array<mixed, int>> $args an array containing the arguments label, value and type: ex: `[":label" => [$value, PDO::PARAM_TYPE]`
|
||||
* @return array<string, mixed>[] the returned rows of the request
|
||||
*/
|
||||
public function fetch(string $query, array $args): array {
|
||||
$stmnt = $this->prep($query, $args);
|
||||
$stmnt->execute();
|
||||
return $stmnt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array<string, array<mixed, int>> $args
|
||||
* @return \PDOStatement
|
||||
*/
|
||||
private function prep(string $query, array $args): \PDOStatement {
|
||||
$stmnt = $this->pdo->prepare($query);
|
||||
foreach ($args as $name => $value) {
|
||||
$stmnt->bindValue($name, $value[0], $value[1]);
|
||||
}
|
||||
return $stmnt;
|
||||
}
|
||||
|
||||
public function prepare(string $query): \PDOStatement {
|
||||
return $this->pdo->prepare($query);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\Core\Data;
|
||||
|
||||
/**
|
||||
* Base class of a user account.
|
||||
* Contains the private information that we don't want
|
||||
* 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
|
||||
*/
|
||||
private string $name;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private int $id;
|
||||
|
||||
|
||||
/**
|
||||
* @param string $email
|
||||
* @param string $name
|
||||
* @param string $token
|
||||
* @param int $id
|
||||
*/
|
||||
public function __construct(string $email, string $name, string $token, int $id) {
|
||||
$this->email = $email;
|
||||
$this->name = $name;
|
||||
$this->token = $token;
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getEmail(): string {
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getToken(): string {
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\Core\Data;
|
||||
|
||||
class TacticInfo {
|
||||
private int $id;
|
||||
private string $name;
|
||||
private int $creationDate;
|
||||
private int $ownerId;
|
||||
|
||||
private string $content;
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @param string $name
|
||||
* @param int $creationDate
|
||||
* @param int $ownerId
|
||||
* @param string $content
|
||||
*/
|
||||
public function __construct(int $id, string $name, int $creationDate, int $ownerId, string $content) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->ownerId = $ownerId;
|
||||
$this->creationDate = $creationDate;
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContent(): string {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOwnerId(): int {
|
||||
return $this->ownerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCreationDate(): int {
|
||||
return $this->creationDate;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\Core\Data;
|
||||
|
||||
class Team {
|
||||
private TeamInfo $info;
|
||||
|
||||
/**
|
||||
* @var Member[] maps users with their role
|
||||
*/
|
||||
private array $members;
|
||||
|
||||
/**
|
||||
* @param TeamInfo $info
|
||||
* @param Member[] $members
|
||||
*/
|
||||
public function __construct(TeamInfo $info, array $members = []) {
|
||||
$this->info = $info;
|
||||
$this->members = $members;
|
||||
}
|
||||
|
||||
public function getInfo(): TeamInfo {
|
||||
return $this->info;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Member[]
|
||||
*/
|
||||
public function listMembers(): array {
|
||||
return $this->members;
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\Core\Data;
|
||||
|
||||
class TeamInfo {
|
||||
private int $id;
|
||||
private string $name;
|
||||
private string $picture;
|
||||
private Color $mainColor;
|
||||
private Color $secondColor;
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @param string $name
|
||||
* @param string $picture
|
||||
* @param Color $mainColor
|
||||
* @param Color $secondColor
|
||||
*/
|
||||
public function __construct(int $id, string $name, string $picture, Color $mainColor, Color $secondColor) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->picture = $picture;
|
||||
$this->mainColor = $mainColor;
|
||||
$this->secondColor = $secondColor;
|
||||
}
|
||||
|
||||
|
||||
public function getId(): int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getPicture(): string {
|
||||
return $this->picture;
|
||||
}
|
||||
|
||||
public function getMainColor(): Color {
|
||||
return $this->mainColor;
|
||||
}
|
||||
|
||||
public function getSecondColor(): Color {
|
||||
return $this->secondColor;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\Core\Gateway;
|
||||
|
||||
use IQBall\Core\Connection;
|
||||
use IQBall\Core\Data\Account;
|
||||
use PDO;
|
||||
|
||||
class AccountGateway {
|
||||
private Connection $con;
|
||||
|
||||
/**
|
||||
* @param Connection $con
|
||||
*/
|
||||
public function __construct(Connection $con) {
|
||||
$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)", [
|
||||
':username' => [$name, PDO::PARAM_STR],
|
||||
':hash' => [$hash, PDO::PARAM_STR],
|
||||
':email' => [$email, PDO::PARAM_STR],
|
||||
':token' => [$token, PDO::PARAM_STR],
|
||||
]);
|
||||
return intval($this->con->lastInsertId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $email
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function getRowsFromMail(string $email): ?array {
|
||||
return $this->con->fetch("SELECT * FROM Account WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]])[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $email
|
||||
* @return string|null the hashed user's password, or null if the given mail does not exist
|
||||
*/
|
||||
public function getHash(string $email): ?string {
|
||||
$results = $this->getRowsFromMail($email);
|
||||
if ($results == null) {
|
||||
return null;
|
||||
}
|
||||
return $results['hash'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $email
|
||||
* @return bool true if the given email exists in the database
|
||||
*/
|
||||
public function exists(string $email): bool {
|
||||
return $this->getRowsFromMail($email) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $email
|
||||
* @return Account|null
|
||||
*/
|
||||
public function getAccountFromMail(string $email): ?Account {
|
||||
$acc = $this->getRowsFromMail($email);
|
||||
if (empty($acc)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Account($email, $acc["username"], $acc["token"], $acc["id"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $token get an account from given token
|
||||
* @return Account|null
|
||||
*/
|
||||
public function getAccountFromToken(string $token): ?Account {
|
||||
$acc = $this->con->fetch("SELECT * FROM Account WHERE token = :token", [':token' => [$token, PDO::PARAM_STR]])[0] ?? null;
|
||||
if (empty($acc)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Account($acc["email"], $acc["username"], $acc["token"], $acc["id"]);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace IQBall\Core\Gateway;
|
||||
|
||||
use IQBall\Core\Connection;
|
||||
use IQBall\Core\Data\Member;
|
||||
use IQBall\Core\Data\MemberRole;
|
||||
use PDO;
|
||||
|
||||
class MemberGateway {
|
||||
private Connection $con;
|
||||
|
||||
/**
|
||||
* @param Connection $con
|
||||
*/
|
||||
public function __construct(Connection $con) {
|
||||
$this->con = $con;
|
||||
}
|
||||
|
||||
/**
|
||||
* insert member to a team
|
||||
* @param int $idTeam
|
||||
* @param int $userId
|
||||
* @param string $role
|
||||
* @return void
|
||||
*/
|
||||
public function insert(int $idTeam, int $userId, string $role): void {
|
||||
$this->con->exec(
|
||||
"INSERT INTO Member(id_team, id_user, role) VALUES (:id_team, :id_user, :role)",
|
||||
[
|
||||
":id_team" => [$idTeam, PDO::PARAM_INT],
|
||||
":id_user" => [$userId, PDO::PARAM_INT],
|
||||
":role" => [$role, PDO::PARAM_STR],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $teamId
|
||||
* @return Member[]
|
||||
*/
|
||||
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",
|
||||
[
|
||||
":id" => [$teamId, PDO::PARAM_INT],
|
||||
]
|
||||
);
|
||||
|
||||
return array_map(fn($row) => new Member($row['id_user'], $row['id_team'], MemberRole::fromName($row['role'])), $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* remove member from given team
|
||||
* @param int $idTeam
|
||||
* @param int $idMember
|
||||
* @return void
|
||||
*/
|
||||
public function remove(int $idTeam, int $idMember): void {
|
||||
$this->con->exec(
|
||||
"DELETE FROM Member WHERE id_team = :id_team AND id_user = :id_user",
|
||||
[
|
||||
":id_team" => [$idTeam, PDO::PARAM_INT],
|
||||
":id_user" => [$idMember, PDO::PARAM_INT],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue