Merge remote-tracking branch 'origin/remove-php' into settings
continuous-integration/drone/push Build is failing
Details
@ -1,2 +1,2 @@
|
|||||||
VITE_API_ENDPOINT=/api
|
VITE_API_ENDPOINT=https://iqball.maxou.dev/api/dotnet-master
|
||||||
VITE_BASE=
|
#VITE_API_ENDPOINT=http://localhost:5254
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2021: true },
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
],
|
||||||
|
ignorePatterns: ["dist", ".eslintrc.cjs"],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["react-refresh"],
|
||||||
|
rules: {
|
||||||
|
"react-refresh/only-export-components": [
|
||||||
|
"warn",
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
@ -1,44 +1,25 @@
|
|||||||
.vs
|
# Logs
|
||||||
.vscode
|
logs
|
||||||
.idea
|
*.log
|
||||||
.code
|
|
||||||
.vite
|
|
||||||
|
|
||||||
vendor
|
|
||||||
.nfs*
|
|
||||||
composer.lock
|
|
||||||
*.phar
|
|
||||||
/dist
|
|
||||||
.guard
|
|
||||||
|
|
||||||
# sqlite database files
|
|
||||||
*.sqlite
|
|
||||||
|
|
||||||
views-mappings.php
|
|
||||||
|
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
.php-cs-fixer.cache
|
package-lock.json
|
@ -1,16 +0,0 @@
|
|||||||
<?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);
|
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"bracketSameLine": true,
|
"bracketSameLine": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"printWidth": 80,
|
"printWidth": 80,
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"semi": false
|
"semi": false
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
master
|
@ -0,0 +1,90 @@
|
|||||||
|
# Welcome on the documentation's description
|
||||||
|
|
||||||
|
## Let's get started with the architecture diagram.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
As you can see our entire application is build around three main package.
|
||||||
|
All of them contained in "src" package.
|
||||||
|
The core represent the main code of the web application.
|
||||||
|
It contains all the validation protocol, detailed below, the model of the imposed MVC architecture.
|
||||||
|
It also has a package named "data", it is a package of the structure of all the data we use in our application.
|
||||||
|
Of course there is package containing all the gateways as its name indicates. It is where we use the connection to our database.
|
||||||
|
Allowing to operate on it.
|
||||||
|
|
||||||
|
The App now is more about the web application itself.
|
||||||
|
Having all the controllers of the MVC architecture the use the model, the validation system and the http system in the core.
|
||||||
|
It also calls the twig's views inside of App. Finally, it uses the package Session. This one replace the $\_SESSION we all know in PHP.
|
||||||
|
Thanks to this we have a way cleaner use of all session's data.
|
||||||
|
Nevertheless, all the controllers call not only twig views but also react ones.
|
||||||
|
Those are present in the package "front", dispatched in several other packages.
|
||||||
|
Such as assets having all the image and stuff, model containing all the data's structure, style centralizing all css file and eventually components the last package used for the editor.
|
||||||
|
|
||||||
|
Finally, we have the package "Api" that allows to share code and bind all the different third-hand application such as the web admin one.
|
||||||
|
|
||||||
|
## Main data class diagram.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can see how our data is structured contained in the package "data" as explained right above.
|
||||||
|
There is two clear part.
|
||||||
|
First of all, the Tactic one.
|
||||||
|
We got a nice class named TacticInfo representing as it says the information about a tactic, nothing to discuss more about.
|
||||||
|
It associates an attribute of type "CourtType". This last is just an "evoluated" type of enum with some more features.
|
||||||
|
We had to do it this way because of the language PHP that doesn't implement such a thing as an enum.
|
||||||
|
|
||||||
|
Now, let's discuss a much bigger part of the diagram.
|
||||||
|
In this part we find all the team logic. Actually, a team only have an array of members and a "TeamInfo".
|
||||||
|
The class "TeamInfo" only exists to split the team's information data (name, id etc) from the members.
|
||||||
|
The type Team does only link the information about a team and its members.
|
||||||
|
Talking about them, their class indicate what role they have (either Coach or Player) in the team.
|
||||||
|
Because a member is registered in the app, therefore he is a user of it. Represented by the type of the same name.
|
||||||
|
This class does only contain all the user's basic information.
|
||||||
|
The last class we have is the Account. It could directly be incorporated in User but we decided to split it the same way we did for the team.
|
||||||
|
Then, Account only has a user and a token which is an identifier.
|
||||||
|
|
||||||
|
## Validation's class diagram
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
We implemented our own validation system, here it is!
|
||||||
|
For the validation methods (for instance those in DefaultValidators) we use lambda to instantiate a Validator.
|
||||||
|
In general, we use the implementation "SimpleFunctionValidator".
|
||||||
|
We reconize the strategy pattern. Indeed, we need a family of algorithms because we have many classes that only differ by the way they validate.
|
||||||
|
Futhermore, you may have notices the ComposedValidator that allows to chain several Validator.
|
||||||
|
We can see that this system uses the composite pattern
|
||||||
|
The other part of the diagram is about the failure a specific field's validation.
|
||||||
|
We have a concrete class to return a something more general. All the successors are just more precise about the failure.
|
||||||
|
|
||||||
|
## Http's class diagram
|
||||||
|
|
||||||
|

|
||||||
|
It were we centralize what the app can render, and what the api can receive.
|
||||||
|
Then, we got the "basic" response (HttpResponse) that just render a HttpCodes.
|
||||||
|
We have two successors for now. ViewHttpResponse render not only a code but also a view, either react or twig ones.
|
||||||
|
Finally, we have the JsonHttpResponse that renders, as it's name says, some Json.
|
||||||
|
|
||||||
|
## Session's class diagram
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
It encapsulates the PHP's array "$\_SESSION". With two interfaces that dictate how a session should be handled, and same for a mutable one.
|
||||||
|
|
||||||
|
## Model View Controller
|
||||||
|
|
||||||
|
All class diagram, separated by their range of action, of the imposed MVC architecture.
|
||||||
|
All of them have a controller that validates entries with the validation system and check the permission the user has,and whether or not actually do the action.
|
||||||
|
These controllers are composed by a Model that handle the pure data and is the point of contact between these and the gateways.
|
||||||
|
Speaking of which, Gateways are composing Models. They use the connection class to access the database and send their query.
|
||||||
|
|
||||||
|
### Team
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Editor
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Authentification
|
||||||
|
|
||||||
|

|
@ -1,3 +1,3 @@
|
|||||||
# The wiki also exists
|
- [Description.md](Description.md)
|
||||||
|
- [Conception.md](Conception.md)
|
||||||
Some of our explanation are contained in the [wiki](https://codefirst.iut.uca.fr/git/IQBall/Application-Web/wiki)
|
- [how-to-dev.md](how-to-dev.md)
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
@startuml
|
||||||
|
'https://plantuml.com/component-diagram
|
||||||
|
|
||||||
|
package front{
|
||||||
|
package assets
|
||||||
|
package components
|
||||||
|
package model
|
||||||
|
package style
|
||||||
|
package views
|
||||||
|
}
|
||||||
|
|
||||||
|
database sql{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
package src {
|
||||||
|
|
||||||
|
package "Api"{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
package "App" {
|
||||||
|
package Controller
|
||||||
|
package Session
|
||||||
|
package Views
|
||||||
|
}
|
||||||
|
|
||||||
|
package Core{
|
||||||
|
package Data
|
||||||
|
package Gateway
|
||||||
|
package Http
|
||||||
|
package Model
|
||||||
|
package Validation
|
||||||
|
[Connection]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[sql] -- [Connection]
|
||||||
|
|
||||||
|
[views] -- [style]
|
||||||
|
[views] -- [components]
|
||||||
|
[views] -- [assets]
|
||||||
|
[views] -- [model]
|
||||||
|
|
||||||
|
[Gateway] -- [Connection]
|
||||||
|
|
||||||
|
[Validation] -- [Controller]
|
||||||
|
[Controller] -- [Session]
|
||||||
|
[Controller] -- [Http]
|
||||||
|
[Controller] -- [Views]
|
||||||
|
[Controller] -- [views]
|
||||||
|
[Controller] -- [Model]
|
||||||
|
[Model] -- [Gateway]
|
||||||
|
|
||||||
|
[Api] -- [Validation]
|
||||||
|
[Api] -- [Model]
|
||||||
|
[Api] -- [Http]
|
||||||
|
|
||||||
|
@enduml
|
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 32 KiB |
@ -0,0 +1,44 @@
|
|||||||
|
@startuml
|
||||||
|
class EditorController {
|
||||||
|
+__construct (model : TacticModel)
|
||||||
|
+ openEditorFor(tactic:TacticInfo): ViewHttpResponse
|
||||||
|
+ createNew(): ViewHttpResponse
|
||||||
|
+ openTestEditor(courtType:CourtType): ViewHttpResponse
|
||||||
|
+ createNewOfKind(type:CourtType, session:SessionHandle): ViewHttpResponse
|
||||||
|
+ openEditor(id:int, session:SessionHandle): ViewHttpResponse
|
||||||
|
}
|
||||||
|
EditorController *-- "- model" TacticModel
|
||||||
|
|
||||||
|
class TacticModel {
|
||||||
|
+ TACTIC_DEFAULT_NAME:int {static}{frozen}
|
||||||
|
+ __construct(tactics : TacticInfoGateway)
|
||||||
|
+ makeNew(name:string, ownerId:int, type:CourtType): TacticInfo
|
||||||
|
+ makeNewDefault(ownerId:int, type:CourtType): ?TacticInfo
|
||||||
|
+ get(id:int): ?TacticInfo
|
||||||
|
+ getLast(nb:int, ownerId:int): array
|
||||||
|
+ getAll(ownerId:int): ?array
|
||||||
|
+ updateName(id:int, name:string, authId:int): array
|
||||||
|
+ updateContent(id:int, json:string): ?ValidationFail
|
||||||
|
}
|
||||||
|
|
||||||
|
TacticModel *-- "- tactics" TacticInfoGateway
|
||||||
|
|
||||||
|
class TacticInfoGateway{
|
||||||
|
+ __construct(con : Connexion)
|
||||||
|
+ get(id:int): ?TacticInfo
|
||||||
|
+ getLast(nb:int, ownerId:int): ?array
|
||||||
|
+ getAll(ownerId:int): ?array
|
||||||
|
+ insert(name:string, owner:int, type:CourtType): int
|
||||||
|
+ updateName(id:int, name:string): bool
|
||||||
|
+ updateContent(id:int, json:string): bool
|
||||||
|
}
|
||||||
|
|
||||||
|
TacticInfoGateway *--"- con" Connexion
|
||||||
|
|
||||||
|
class TacticValidator{
|
||||||
|
+ validateAccess(tacticId:int, tactic:?TacticInfo, ownerId:int): ?ValidationFail {static}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorController ..> TacticValidator
|
||||||
|
|
||||||
|
@enduml
|
@ -1,63 +1,87 @@
|
|||||||
@startuml
|
@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{
|
class TeamGateway{
|
||||||
--
|
|
||||||
+ __construct(con : Connexion)
|
+ __construct(con : Connexion)
|
||||||
+ insert(name : string ,picture : string, mainColor : Color, secondColor : Color)
|
+ insert(name : string ,picture : string, mainColor : Color, secondColor : Color)
|
||||||
+ listByName(name : string): array
|
+ listByName(name : string): array
|
||||||
|
+ getTeamById(id:int): ?TeamInfo
|
||||||
|
+ getTeamIdByName(name:string): ?int
|
||||||
|
+ deleteTeam(idTeam:int): void
|
||||||
|
+ editTeam(idTeam:int, newName:string, newPicture:string, newMainColor:string, newSecondColor:string)
|
||||||
|
+ getAll(user:int): array
|
||||||
}
|
}
|
||||||
|
|
||||||
TeamGateway *--"- con" Connexion
|
TeamGateway *--"- con" Connexion
|
||||||
TeamGateway ..> Color
|
|
||||||
|
|
||||||
|
class MemberGateway{
|
||||||
|
|
||||||
|
+ __construct(con : Connexion)
|
||||||
|
+ insert(idTeam:int, userId:int, role:string): void
|
||||||
|
+ getMembersOfTeam(teamId:int): array
|
||||||
|
+ remove(idTeam:int, idMember:int): void
|
||||||
|
+ isCoach(email:string, idTeam:int): bool
|
||||||
|
+ isMemberOfTeam(idTeam:int, idCurrentUser:int): bool
|
||||||
|
}
|
||||||
|
|
||||||
|
MemberGateway *--"- con" Connexion
|
||||||
|
|
||||||
|
class AccountGateway{
|
||||||
|
+ __construct(con : Connexion)
|
||||||
|
+ insertAccount(name:string, email:string, token:string, hash:string, profilePicture:string): int
|
||||||
|
+ getRowsFromMail(email:string): ?array
|
||||||
|
+ getHash(email:string): ?string
|
||||||
|
+ exists(email:string): bool
|
||||||
|
+ getAccountFromMail(email:string): ?Account
|
||||||
|
+ getAccountFromToken(token:string): ?Account
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountGateway *--"- con" Connexion
|
||||||
|
|
||||||
class TeamModel{
|
class TeamModel{
|
||||||
---
|
|
||||||
+ __construct(gateway : TeamGateway)
|
+ __construct(gateway : TeamGateway)
|
||||||
+ createTeam(name : string,picture : string, mainColorValue : int, secondColorValue : int, errors : array)
|
+ createTeam(name : string,picture : string, mainColorValue : int, secondColorValue : int, errors : array)
|
||||||
|
+ addMember(mail:string, teamId:int, role:string): int
|
||||||
+ listByName(name : string ,errors : array) : ?array
|
+ listByName(name : string ,errors : array) : ?array
|
||||||
+ displayTeam(id : int): Team
|
+ getTeam(idTeam:int, idCurrentUser:int): ?Team
|
||||||
|
+ deleteMember(idMember:int, teamId:int): int
|
||||||
|
+ deleteTeam(email:string, idTeam:int): int
|
||||||
|
+ isCoach(idTeam:int, email:string): bool
|
||||||
|
+ editTeam(idTeam:int, newName:string, newPicture:string, newMainColor:string, newSecondColor:string)
|
||||||
|
+ getAll(user:int): array
|
||||||
}
|
}
|
||||||
|
|
||||||
TeamModel *--"- gateway" TeamGateway
|
TeamModel *--"- members" MemberGateway
|
||||||
TeamModel ..> Team
|
TeamModel *--"- teams" TeamGateway
|
||||||
TeamModel ..> Color
|
TeamModel *--"- teams" AccountGateway
|
||||||
|
|
||||||
|
|
||||||
class TeamController{
|
class TeamController{
|
||||||
- twig : Environement
|
+ __construct( model : TeamModel)
|
||||||
--
|
+ displayCreateTeam(session:SessionHandle): ViewHttpResponse
|
||||||
+ __construct( model : TeamModel, twig : Environement)
|
+ displayDeleteMember(session:SessionHandle): ViewHttpResponse
|
||||||
+ displaySubmitTeam() : HttpResponse
|
+ submitTeam(request:array, session:SessionHandle): HttpResponse
|
||||||
+ submitTeam(request : array) : HttpResponse
|
+ displayListTeamByName(session:SessionHandle): ViewHttpResponse
|
||||||
+ displayListTeamByName(): HttpResponse
|
+ listTeamByName(request:array, session:SessionHandle): HttpResponse
|
||||||
+ listTeamByName(request : array) : HttpResponse
|
+ deleteTeamById(id:int, session:SessionHandle): HttpResponse
|
||||||
+ displayTeam(id : int): HttpResponse
|
+ displayTeam(id:int, session:SessionHandle): ViewHttpResponse
|
||||||
|
+ displayAddMember(idTeam:int, session:SessionHandle): ViewHttpResponse
|
||||||
|
+ addMember(idTeam:int, request:array, session:SessionHandle): HttpResponse
|
||||||
|
+ deleteMember(idTeam:int, idMember:int, session:SessionHandle): HttpResponse
|
||||||
|
+ displayEditTeam(idTeam:int, session:SessionHandle): ViewHttpResponse
|
||||||
|
+ editTeam(idTeam:int, request:array, session:SessionHandle): HttpResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
TeamController *--"- model" TeamModel
|
TeamController *--"- model" TeamModel
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Connexion { }
|
class Connexion { }
|
||||||
|
|
||||||
@enduml
|
@enduml
|
@ -0,0 +1,27 @@
|
|||||||
|
@startuml
|
||||||
|
|
||||||
|
interface SessionHandle{
|
||||||
|
+ getInitialTarget(): ?string {abstract}
|
||||||
|
+ getAccount(): ?Account {abstract}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MutableSessionHandle{
|
||||||
|
+ setInitialTarget(url:?string): void
|
||||||
|
+ setAccount(account:Account): void
|
||||||
|
+ destroy(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
class PhpSessionHandle{
|
||||||
|
+ init(): self {static}
|
||||||
|
+ getAccount(): ?Account
|
||||||
|
+ getInitialTarget(): ?string
|
||||||
|
+ setAccount(account:Account): void
|
||||||
|
+ setInitialTarget(url:?string): void
|
||||||
|
+ destroy(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PhpSessionHandle ..|> MutableSessionHandle
|
||||||
|
MutableSessionHandle ..|> SessionHandle
|
||||||
|
|
||||||
|
@enduml
|
@ -0,0 +1,18 @@
|
|||||||
|
set -e
|
||||||
|
|
||||||
|
export OUTPUT=$1
|
||||||
|
export BASE=$2
|
||||||
|
|
||||||
|
rm -rf "$OUTPUT"/*
|
||||||
|
|
||||||
|
echo "VITE_API_ENDPOINT=$BASE/api" >> .env.PROD
|
||||||
|
echo "VITE_BASE=$BASE" >> .env.PROD
|
||||||
|
|
||||||
|
ci/build_react.msh
|
||||||
|
|
||||||
|
mkdir -p $OUTPUT/profiles/
|
||||||
|
|
||||||
|
sed -E 's/\/\*PROFILE_FILE\*\/\s*".*"/"profiles\/prod-config-profile.php"/' config.php > $OUTPUT/config.php
|
||||||
|
sed -E "s/const BASE_PATH = .*;/const BASE_PATH = \"$(sed s/\\//\\\\\\//g <<< "$BASE")\";/" profiles/prod-config-profile.php > $OUTPUT/profiles/prod-config-profile.php
|
||||||
|
|
||||||
|
cp -r vendor sql src public $OUTPUT
|
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"IQBall\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"altorouter/altorouter": "1.2.0",
|
|
||||||
"ext-json": "*",
|
|
||||||
"ext-pdo": "*",
|
|
||||||
"ext-pdo_sqlite": "*",
|
|
||||||
"twig/twig":"^2.0",
|
|
||||||
"phpstan/phpstan": "*"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"friendsofphp/php-cs-fixer": "^3.38"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
#!/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,19 +0,0 @@
|
|||||||
import ReactDOM from "react-dom/client"
|
|
||||||
import React, { FunctionComponent } from "react"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dynamically renders a React component, with given arguments
|
|
||||||
* @param Component the react component to render
|
|
||||||
* @param args the arguments to pass to the react component.
|
|
||||||
*/
|
|
||||||
export function renderView(Component: FunctionComponent, args: {}) {
|
|
||||||
const root = ReactDOM.createRoot(
|
|
||||||
document.getElementById("root") as HTMLElement,
|
|
||||||
)
|
|
||||||
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<Component {...args} />
|
|
||||||
</React.StrictMode>,
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,272 +0,0 @@
|
|||||||
import { CourtBall } from "./CourtBall"
|
|
||||||
|
|
||||||
import {
|
|
||||||
ReactElement,
|
|
||||||
RefObject,
|
|
||||||
useCallback,
|
|
||||||
useLayoutEffect,
|
|
||||||
useState,
|
|
||||||
} from "react"
|
|
||||||
import CourtPlayer from "./CourtPlayer"
|
|
||||||
|
|
||||||
import { Player } from "../../model/tactic/Player"
|
|
||||||
import { Action, ActionKind } from "../../model/tactic/Action"
|
|
||||||
import ArrowAction from "../actions/ArrowAction"
|
|
||||||
import { middlePos, ratioWithinBase } from "../arrows/Pos"
|
|
||||||
import BallAction from "../actions/BallAction"
|
|
||||||
import { CourtObject } from "../../model/tactic/Ball"
|
|
||||||
import { contains } from "../arrows/Box"
|
|
||||||
import { CourtAction } from "../../views/editor/CourtAction"
|
|
||||||
|
|
||||||
export interface BasketCourtProps {
|
|
||||||
players: Player[]
|
|
||||||
actions: Action[]
|
|
||||||
objects: CourtObject[]
|
|
||||||
|
|
||||||
renderAction: (a: Action, key: number) => ReactElement
|
|
||||||
setActions: (f: (a: Action[]) => Action[]) => void
|
|
||||||
|
|
||||||
onPlayerRemove: (p: Player) => void
|
|
||||||
onPlayerChange: (p: Player) => void
|
|
||||||
|
|
||||||
onBallRemove: () => void
|
|
||||||
onBallMoved: (ball: DOMRect) => void
|
|
||||||
|
|
||||||
courtImage: ReactElement
|
|
||||||
courtRef: RefObject<HTMLDivElement>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BasketCourt({
|
|
||||||
players,
|
|
||||||
actions,
|
|
||||||
objects,
|
|
||||||
renderAction,
|
|
||||||
setActions,
|
|
||||||
onPlayerRemove,
|
|
||||||
onPlayerChange,
|
|
||||||
|
|
||||||
onBallMoved,
|
|
||||||
onBallRemove,
|
|
||||||
|
|
||||||
courtImage,
|
|
||||||
courtRef,
|
|
||||||
}: BasketCourtProps) {
|
|
||||||
function placeArrow(origin: Player, arrowHead: DOMRect) {
|
|
||||||
const originRef = document.getElementById(origin.id)!
|
|
||||||
const courtBounds = courtRef.current!.getBoundingClientRect()
|
|
||||||
const start = ratioWithinBase(
|
|
||||||
middlePos(originRef.getBoundingClientRect()),
|
|
||||||
courtBounds,
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const player of players) {
|
|
||||||
if (player.id == origin.id) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const playerBounds = document
|
|
||||||
.getElementById(player.id)!
|
|
||||||
.getBoundingClientRect()
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
playerBounds.top > arrowHead.bottom ||
|
|
||||||
playerBounds.right < arrowHead.left ||
|
|
||||||
playerBounds.bottom < arrowHead.top ||
|
|
||||||
playerBounds.left > arrowHead.right
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const targetPos = document
|
|
||||||
.getElementById(player.id)!
|
|
||||||
.getBoundingClientRect()
|
|
||||||
|
|
||||||
const end = ratioWithinBase(middlePos(targetPos), courtBounds)
|
|
||||||
|
|
||||||
const action: Action = {
|
|
||||||
fromPlayerId: originRef.id,
|
|
||||||
toPlayerId: player.id,
|
|
||||||
type: origin.hasBall ? ActionKind.SHOOT : ActionKind.SCREEN,
|
|
||||||
moveFrom: start,
|
|
||||||
segments: [{ next: end }],
|
|
||||||
}
|
|
||||||
setActions((actions) => [...actions, action])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const action: Action = {
|
|
||||||
fromPlayerId: originRef.id,
|
|
||||||
type: origin.hasBall ? ActionKind.DRIBBLE : ActionKind.MOVE,
|
|
||||||
moveFrom: ratioWithinBase(
|
|
||||||
middlePos(originRef.getBoundingClientRect()),
|
|
||||||
courtBounds,
|
|
||||||
),
|
|
||||||
segments: [
|
|
||||||
{ next: ratioWithinBase(middlePos(arrowHead), courtBounds) },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
setActions((actions) => [...actions, action])
|
|
||||||
}
|
|
||||||
|
|
||||||
const [previewAction, setPreviewAction] = useState<Action | null>(null)
|
|
||||||
|
|
||||||
const updateActionsRelatedTo = useCallback((player: Player) => {
|
|
||||||
const newPos = ratioWithinBase(
|
|
||||||
middlePos(
|
|
||||||
document.getElementById(player.id)!.getBoundingClientRect(),
|
|
||||||
),
|
|
||||||
courtRef.current!.getBoundingClientRect(),
|
|
||||||
)
|
|
||||||
setActions((actions) =>
|
|
||||||
actions.map((a) => {
|
|
||||||
if (a.fromPlayerId == player.id) {
|
|
||||||
return { ...a, moveFrom: newPos }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a.toPlayerId == player.id) {
|
|
||||||
const segments = a.segments.toSpliced(
|
|
||||||
a.segments.length - 1,
|
|
||||||
1,
|
|
||||||
{
|
|
||||||
...a.segments[a.segments.length - 1],
|
|
||||||
next: newPos,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return { ...a, segments }
|
|
||||||
}
|
|
||||||
|
|
||||||
return a
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const [internActions, setInternActions] = useState<Action[]>([])
|
|
||||||
|
|
||||||
useLayoutEffect(() => setInternActions(actions), [actions])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="court-container"
|
|
||||||
ref={courtRef}
|
|
||||||
style={{ position: "relative" }}>
|
|
||||||
{courtImage}
|
|
||||||
|
|
||||||
{players.map((player) => (
|
|
||||||
<CourtPlayer
|
|
||||||
key={player.id}
|
|
||||||
player={player}
|
|
||||||
onDrag={() => updateActionsRelatedTo(player)}
|
|
||||||
onChange={onPlayerChange}
|
|
||||||
onRemove={() => onPlayerRemove(player)}
|
|
||||||
courtRef={courtRef}
|
|
||||||
availableActions={(pieceRef) => [
|
|
||||||
<ArrowAction
|
|
||||||
key={1}
|
|
||||||
onHeadMoved={(headPos) => {
|
|
||||||
const baseBounds =
|
|
||||||
courtRef.current!.getBoundingClientRect()
|
|
||||||
|
|
||||||
const arrowHeadPos = middlePos(headPos)
|
|
||||||
|
|
||||||
const target = players.find(
|
|
||||||
(p) =>
|
|
||||||
p != player &&
|
|
||||||
contains(
|
|
||||||
document
|
|
||||||
.getElementById(p.id)!
|
|
||||||
.getBoundingClientRect(),
|
|
||||||
arrowHeadPos,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
setPreviewAction((action) => ({
|
|
||||||
...action!,
|
|
||||||
segments: [
|
|
||||||
{
|
|
||||||
next: ratioWithinBase(
|
|
||||||
arrowHeadPos,
|
|
||||||
baseBounds,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
type: player.hasBall
|
|
||||||
? target
|
|
||||||
? ActionKind.SHOOT
|
|
||||||
: ActionKind.DRIBBLE
|
|
||||||
: target
|
|
||||||
? ActionKind.SCREEN
|
|
||||||
: ActionKind.MOVE,
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
onHeadPicked={(headPos) => {
|
|
||||||
;(document.activeElement as HTMLElement).blur()
|
|
||||||
const baseBounds =
|
|
||||||
courtRef.current!.getBoundingClientRect()
|
|
||||||
|
|
||||||
setPreviewAction({
|
|
||||||
type: player.hasBall
|
|
||||||
? ActionKind.DRIBBLE
|
|
||||||
: ActionKind.MOVE,
|
|
||||||
fromPlayerId: player.id,
|
|
||||||
toPlayerId: undefined,
|
|
||||||
moveFrom: ratioWithinBase(
|
|
||||||
middlePos(
|
|
||||||
pieceRef.getBoundingClientRect(),
|
|
||||||
),
|
|
||||||
baseBounds,
|
|
||||||
),
|
|
||||||
segments: [
|
|
||||||
{
|
|
||||||
next: ratioWithinBase(
|
|
||||||
middlePos(headPos),
|
|
||||||
baseBounds,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
onHeadDropped={(headRect) => {
|
|
||||||
placeArrow(player, headRect)
|
|
||||||
setPreviewAction(null)
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
player.hasBall && (
|
|
||||||
<BallAction
|
|
||||||
key={2}
|
|
||||||
onDrop={(ref) =>
|
|
||||||
onBallMoved(ref.getBoundingClientRect())
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{internActions.map((action, idx) => renderAction(action, idx))}
|
|
||||||
|
|
||||||
{objects.map((object) => {
|
|
||||||
if (object.type == "ball") {
|
|
||||||
return (
|
|
||||||
<CourtBall
|
|
||||||
onMoved={onBallMoved}
|
|
||||||
ball={object}
|
|
||||||
onRemove={onBallRemove}
|
|
||||||
key="ball"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
throw new Error("unknown court object" + object.type)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{previewAction && (
|
|
||||||
<CourtAction
|
|
||||||
courtRef={courtRef}
|
|
||||||
action={previewAction}
|
|
||||||
//do nothing on change, not really possible as it's a preview arrow
|
|
||||||
onActionDeleted={() => {}}
|
|
||||||
onActionChanges={() => {}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export type CourtObject = { type: "ball" } & Ball
|
|
||||||
|
|
||||||
export interface Ball {
|
|
||||||
/**
|
|
||||||
* The ball is a "ball" court object
|
|
||||||
*/
|
|
||||||
readonly type: "ball"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
|
|
||||||
*/
|
|
||||||
readonly bottomRatio: number
|
|
||||||
/**
|
|
||||||
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
|
|
||||||
*/
|
|
||||||
readonly rightRatio: number
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
export type PlayerId = string
|
|
||||||
|
|
||||||
export enum PlayerTeam {
|
|
||||||
Allies = "allies",
|
|
||||||
Opponents = "opponents",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Player {
|
|
||||||
readonly id: PlayerId
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the player's team
|
|
||||||
* */
|
|
||||||
readonly team: PlayerTeam
|
|
||||||
|
|
||||||
/**
|
|
||||||
* player's role
|
|
||||||
* */
|
|
||||||
readonly role: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
|
|
||||||
*/
|
|
||||||
readonly bottomRatio: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
|
|
||||||
*/
|
|
||||||
readonly rightRatio: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if the player has a basketball
|
|
||||||
*/
|
|
||||||
readonly hasBall: boolean
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import { Player } from "./Player"
|
|
||||||
import { CourtObject } from "./Ball"
|
|
||||||
import { Action } from "./Action"
|
|
||||||
|
|
||||||
export interface Tactic {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
content: TacticContent
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TacticContent {
|
|
||||||
players: Player[]
|
|
||||||
objects: CourtObject[]
|
|
||||||
actions: Action[]
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
:root {
|
|
||||||
--main-color: #191a21;
|
|
||||||
--second-color: #282a36;
|
|
||||||
--third-color: #303341;
|
|
||||||
--accent-color: #ffa238;
|
|
||||||
--main-contrast-color: #e6edf3;
|
|
||||||
--font-title: Helvetica;
|
|
||||||
--font-content: Helvetica;
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
import React, { CSSProperties, useState } from "react"
|
|
||||||
import "../style/visualizer.css"
|
|
||||||
import Court from "../assets/court/full_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,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/src/assets/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>IQBall</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,12 +0,0 @@
|
|||||||
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
|
|
@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
$hostname = getHostName();
|
|
||||||
$front_url = "http://$hostname:5173";
|
|
||||||
|
|
||||||
const _SUPPORTS_FAST_REFRESH = true;
|
|
||||||
$_data_source_name = "sqlite:${_SERVER['DOCUMENT_ROOT']}/../dev-database.sqlite";
|
|
||||||
|
|
||||||
// no user and password needed for sqlite databases
|
|
||||||
const _DATABASE_USER = null;
|
|
||||||
const _DATABASE_PASSWORD = null;
|
|
||||||
|
|
||||||
function _asset(string $assetURI): string {
|
|
||||||
global $front_url;
|
|
||||||
return $front_url . "/" . $assetURI;
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// This file only exists on production servers, and defines the available assets mappings
|
|
||||||
// in an `ASSETS` array constant.
|
|
||||||
require __DIR__ . "/../views-mappings.php";
|
|
||||||
|
|
||||||
const _SUPPORTS_FAST_REFRESH = false;
|
|
||||||
$database_file = __DIR__ . "/../database.sqlite";
|
|
||||||
$_data_source_name = "sqlite:/$database_file";
|
|
||||||
|
|
||||||
// no user and password needed for sqlite databases
|
|
||||||
const _DATABASE_USER = null;
|
|
||||||
const _DATABASE_PASSWORD = null;
|
|
||||||
|
|
||||||
|
|
||||||
function _asset(string $assetURI): string {
|
|
||||||
// use index.php's base path
|
|
||||||
global $basePath;
|
|
||||||
// If the asset uri does not figure in the available assets array,
|
|
||||||
// fallback to the uri itself.
|
|
||||||
return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI);
|
|
||||||
}
|
|
Before Width: | Height: | Size: 747 B |
@ -1 +0,0 @@
|
|||||||
../front/assets
|
|
@ -1 +0,0 @@
|
|||||||
../front
|
|
@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return PDO The PDO instance of the configuration's database connexion.
|
|
||||||
*/
|
|
||||||
function get_database(): PDO {
|
|
||||||
// defined by profiles.
|
|
||||||
global $data_source_name;
|
|
||||||
$pdo = new PDO($data_source_name, DATABASE_USER, DATABASE_PASSWORD, [PDO::ERRMODE_EXCEPTION]);
|
|
||||||
|
|
||||||
$database_exists = $pdo->query("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'")->fetchColumn() > 0;
|
|
||||||
|
|
||||||
if ($database_exists) {
|
|
||||||
return $pdo;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (scandir(__DIR__) as $file) {
|
|
||||||
if (preg_match("/.*\.sql$/i", $file)) {
|
|
||||||
$content = file_get_contents(__DIR__ . "/" . $file);
|
|
||||||
|
|
||||||
$pdo->exec($content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return $pdo;
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
<?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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
<?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()]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
<?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->getUser()->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,67 @@
|
|||||||
|
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom"
|
||||||
|
|
||||||
|
import { Header } from "./pages/template/Header.tsx"
|
||||||
|
import "./style/app.css"
|
||||||
|
import { lazy } from "react"
|
||||||
|
import { BASE } from "./Constants.ts"
|
||||||
|
|
||||||
|
const HomePage = lazy(() => import("./pages/HomePage.tsx"))
|
||||||
|
const LoginPage = lazy(() => import("./pages/LoginPage.tsx"))
|
||||||
|
const RegisterPage = lazy(() => import("./pages/RegisterPage.tsx"))
|
||||||
|
const NotFoundPage = lazy(() => import("./pages/404.tsx"))
|
||||||
|
const CreateTeamPage = lazy(() => import("./pages/CreateTeamPage.tsx"))
|
||||||
|
const TeamPanelPage = lazy(() => import("./pages/TeamPanel.tsx"))
|
||||||
|
const NewTacticPage = lazy(() => import("./pages/NewTacticPage.tsx"))
|
||||||
|
const Editor = lazy(() => import("./pages/Editor.tsx"))
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<div id="app">
|
||||||
|
<BrowserRouter basename={BASE}>
|
||||||
|
<Outlet />
|
||||||
|
|
||||||
|
<Routes>
|
||||||
|
<Route path={"/login"} element={<LoginPage />} />
|
||||||
|
<Route path={"/register"} element={<RegisterPage />} />
|
||||||
|
|
||||||
|
<Route path={"/"} element={<AppLayout />}>
|
||||||
|
<Route path={"/"} element={<HomePage />} />
|
||||||
|
<Route path={"/home"} element={<HomePage />} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={"/team/new"}
|
||||||
|
element={<CreateTeamPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/team/:teamId"}
|
||||||
|
element={<TeamPanelPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/tactic/new"}
|
||||||
|
element={<NewTacticPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/tactic/:tacticId/edit"}
|
||||||
|
element={<Editor guestMode={false} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/tactic/edit-guest"}
|
||||||
|
element={<Editor guestMode={true} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path={"*"} element={<NotFoundPage />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,92 +0,0 @@
|
|||||||
<?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,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\App;
|
|
||||||
|
|
||||||
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<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): 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");
|
|
||||||
return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST);
|
|
||||||
}
|
|
||||||
$payload = get_object_vars($payload_obj);
|
|
||||||
return self::runCheckedFrom($payload, $schema, $run);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs given callback, if the given request data array validates 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): HttpResponse {
|
|
||||||
$fails = [];
|
|
||||||
$request = HttpRequest::from($data, $fails, $schema);
|
|
||||||
|
|
||||||
if (!empty($fails)) {
|
|
||||||
return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
return call_user_func_array($run, [$request]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,246 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\App\Controller;
|
|
||||||
|
|
||||||
use IQBall\App\Session\SessionHandle;
|
|
||||||
use IQBall\App\ViewHttpResponse;
|
|
||||||
use IQBall\Core\Data\Account;
|
|
||||||
use IQBall\Core\Http\HttpCodes;
|
|
||||||
use IQBall\Core\Http\HttpRequest;
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
use IQBall\Core\Model\TeamModel;
|
|
||||||
use IQBall\Core\Validation\FieldValidationFail;
|
|
||||||
use IQBall\Core\Validation\ValidationFail;
|
|
||||||
use IQBall\Core\Validation\Validators;
|
|
||||||
|
|
||||||
class TeamController {
|
|
||||||
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 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']);
|
|
||||||
$this->model->addMember($session->getAccount()->getUser()->getEmail(), $teamId, 'COACH');
|
|
||||||
return HttpResponse::redirect('/team/' . $teamId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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'], $session->getAccount()->getUser()->getId());
|
|
||||||
|
|
||||||
if (empty($teams)) {
|
|
||||||
return ViewHttpResponse::twig('display_teams.html.twig', []);
|
|
||||||
}
|
|
||||||
return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $teams]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a team with its id
|
|
||||||
* @param int $id
|
|
||||||
* @param SessionHandle $session
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function deleteTeamById(int $id, SessionHandle $session): HttpResponse {
|
|
||||||
$a = $session->getAccount();
|
|
||||||
$ret = $this->model->deleteTeam($a->getUser()->getEmail(), $id);
|
|
||||||
if($ret != 0) {
|
|
||||||
return ViewHttpResponse::twig('display_team.html.twig', ['notDeleted' => true]);
|
|
||||||
}
|
|
||||||
return HttpResponse::redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a team with its id
|
|
||||||
* @param int $id
|
|
||||||
* @param SessionHandle $session
|
|
||||||
* @return ViewHttpResponse a view that displays given team information
|
|
||||||
*/
|
|
||||||
public function displayTeam(int $id, SessionHandle $session): ViewHttpResponse {
|
|
||||||
$result = $this->model->getTeam($id, $session->getAccount()->getUser()->getId());
|
|
||||||
if($result == null) {
|
|
||||||
return ViewHttpResponse::twig('error.html.twig', [
|
|
||||||
'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette équipe.")],
|
|
||||||
], HttpCodes::FORBIDDEN);
|
|
||||||
}
|
|
||||||
$role = $this->model->isCoach($id, $session->getAccount()->getUser()->getEmail());
|
|
||||||
|
|
||||||
return ViewHttpResponse::react(
|
|
||||||
'views/TeamPanel.tsx',
|
|
||||||
[
|
|
||||||
'team' => [
|
|
||||||
"info" => $result->getInfo(),
|
|
||||||
"members" => $result->listMembers(),
|
|
||||||
],
|
|
||||||
'isCoach' => $role,
|
|
||||||
'currentUserId' => $session->getAccount()->getUser()->getId()]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $idTeam
|
|
||||||
* @param SessionHandle $session
|
|
||||||
* @return ViewHttpResponse the team panel to add a member
|
|
||||||
*/
|
|
||||||
public function displayAddMember(int $idTeam, SessionHandle $session): ViewHttpResponse {
|
|
||||||
return ViewHttpResponse::twig("add_member.html.twig", ['idTeam' => $idTeam]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add a member to a team
|
|
||||||
* @param int $idTeam
|
|
||||||
* @param array<string, mixed> $request
|
|
||||||
* @param SessionHandle $session
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function addMember(int $idTeam, array $request, SessionHandle $session): HttpResponse {
|
|
||||||
$errors = [];
|
|
||||||
if(!$this->model->isCoach($idTeam, $session->getAccount()->getUser()->getEmail())) {
|
|
||||||
return ViewHttpResponse::twig('error.html.twig', [
|
|
||||||
'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette action pour cette équipe.")],
|
|
||||||
], HttpCodes::FORBIDDEN);
|
|
||||||
}
|
|
||||||
$request = HttpRequest::from($request, $errors, [
|
|
||||||
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
|
|
||||||
]);
|
|
||||||
if(!empty($errors)) {
|
|
||||||
return ViewHttpResponse::twig('add_member.html.twig', ['badEmail' => true,'idTeam' => $idTeam]);
|
|
||||||
}
|
|
||||||
$ret = $this->model->addMember($request['email'], $idTeam, $request['role']);
|
|
||||||
|
|
||||||
switch($ret) {
|
|
||||||
case -1:
|
|
||||||
return ViewHttpResponse::twig('add_member.html.twig', ['notFound' => true,'idTeam' => $idTeam]);
|
|
||||||
case -2:
|
|
||||||
return ViewHttpResponse::twig('add_member.html.twig', ['alreadyExisting' => true,'idTeam' => $idTeam]);
|
|
||||||
default:
|
|
||||||
return HttpResponse::redirect('/team/' . $idTeam);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* remove a member from a team with their ids
|
|
||||||
* @param int $idTeam
|
|
||||||
* @param int $idMember
|
|
||||||
* @param SessionHandle $session
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function deleteMember(int $idTeam, int $idMember, SessionHandle $session): HttpResponse {
|
|
||||||
if(!$this->model->isCoach($idTeam, $session->getAccount()->getUser()->getEmail())) {
|
|
||||||
return ViewHttpResponse::twig('error.html.twig', [
|
|
||||||
'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette action pour cette équipe.")],
|
|
||||||
], HttpCodes::FORBIDDEN);
|
|
||||||
}
|
|
||||||
$teamId = $this->model->deleteMember($idMember, $idTeam);
|
|
||||||
if($teamId == -1 || $session->getAccount()->getUser()->getId() == $idMember) {
|
|
||||||
return HttpResponse::redirect('/');
|
|
||||||
}
|
|
||||||
return $this->displayTeam($teamId, $session);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $idTeam
|
|
||||||
* @param SessionHandle $session
|
|
||||||
* @return ViewHttpResponse
|
|
||||||
*/
|
|
||||||
public function displayEditTeam(int $idTeam, SessionHandle $session): ViewHttpResponse {
|
|
||||||
return ViewHttpResponse::twig("edit_team.html.twig", ['team' => $this->model->getTeam($idTeam, $session->getAccount()->getUser()->getId())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $idTeam
|
|
||||||
* @param array<string,mixed> $request
|
|
||||||
* @param SessionHandle $session
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function editTeam(int $idTeam, array $request, SessionHandle $session): HttpResponse {
|
|
||||||
if(!$this->model->isCoach($idTeam, $session->getAccount()->getUser()->getEmail())) {
|
|
||||||
return ViewHttpResponse::twig('error.html.twig', [
|
|
||||||
'failures' => [ValidationFail::unauthorized("Vous n'avez pas accès à cette action pour cette équipe.")],
|
|
||||||
], HttpCodes::FORBIDDEN);
|
|
||||||
}
|
|
||||||
$failures = [];
|
|
||||||
$request = HttpRequest::from($request, $failures, [
|
|
||||||
"name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()],
|
|
||||||
"main_color" => [Validators::hexColor()],
|
|
||||||
"second_color" => [Validators::hexColor()],
|
|
||||||
"picture" => [Validators::isURL()],
|
|
||||||
]);
|
|
||||||
if (!empty($failures)) {
|
|
||||||
$badFields = [];
|
|
||||||
foreach ($failures as $e) {
|
|
||||||
if ($e instanceof FieldValidationFail) {
|
|
||||||
$badFields[] = $e->getFieldName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ViewHttpResponse::twig('edit_team.html.twig', ['bad_fields' => $badFields]);
|
|
||||||
}
|
|
||||||
$this->model->editTeam($idTeam, $request['name'], $request['picture'], $request['main_color'], $request['second_color']);
|
|
||||||
return HttpResponse::redirect('/team/' . $idTeam);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
<?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()->getUser()->getId());
|
|
||||||
|
|
||||||
if ($failure != null) {
|
|
||||||
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
<?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;
|
|
||||||
|
|
||||||
public function destroy(): void;
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(): void {
|
|
||||||
session_destroy();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\App;
|
|
||||||
|
|
||||||
use IQBall\Core\Http\HttpCodes;
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
|
|
||||||
class ViewHttpResponse extends HttpResponse {
|
|
||||||
public const TWIG_VIEW = 0;
|
|
||||||
public const REACT_VIEW = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string File path of the responded view
|
|
||||||
*/
|
|
||||||
private string $file;
|
|
||||||
/**
|
|
||||||
* @var array<string, mixed> View arguments
|
|
||||||
*/
|
|
||||||
private array $arguments;
|
|
||||||
/**
|
|
||||||
* @var int Kind of view, see {@link self::TWIG_VIEW} and {@link self::REACT_VIEW}
|
|
||||||
*/
|
|
||||||
private int $kind;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $code
|
|
||||||
* @param int $kind
|
|
||||||
* @param string $file
|
|
||||||
* @param array<string, mixed> $arguments
|
|
||||||
*/
|
|
||||||
private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) {
|
|
||||||
parent::__construct($code, []);
|
|
||||||
$this->kind = $kind;
|
|
||||||
$this->file = $file;
|
|
||||||
$this->arguments = $arguments;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getViewKind(): int {
|
|
||||||
return $this->kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFile(): string {
|
|
||||||
return $this->file;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public function getArguments(): array {
|
|
||||||
return $this->arguments;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a twig view response
|
|
||||||
* @param string $file
|
|
||||||
* @param array<string, mixed> $arguments
|
|
||||||
* @param int $code
|
|
||||||
* @return ViewHttpResponse
|
|
||||||
*/
|
|
||||||
public static function twig(string $file, array $arguments, int $code = HttpCodes::OK): ViewHttpResponse {
|
|
||||||
return new ViewHttpResponse(self::TWIG_VIEW, $file, $arguments, $code);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a react view response
|
|
||||||
* @param string $file
|
|
||||||
* @param array<string, mixed> $arguments
|
|
||||||
* @param int $code
|
|
||||||
* @return ViewHttpResponse
|
|
||||||
*/
|
|
||||||
public static function react(string $file, array $arguments, int $code = HttpCodes::OK): ViewHttpResponse {
|
|
||||||
return new ViewHttpResponse(self::REACT_VIEW, $file, $arguments, $code);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
<!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>
|
|
@ -1,118 +0,0 @@
|
|||||||
<!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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.failed{
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1><a href="{{ path('/') }}">IQBall</a></h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<h2>Ajouter un membre à votre équipe</h2>
|
|
||||||
<form action="{{ path("/team/#{idTeam}/addMember") }}" method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
|
|
||||||
<label for="email">Email du membre :</label>
|
|
||||||
{% if badEmail %}
|
|
||||||
<p class="failed">Email invalide</p>
|
|
||||||
{% endif %}
|
|
||||||
{%if notFound %}
|
|
||||||
<p class="failed">Cette personne n'a pas été trouvé</p>
|
|
||||||
{% endif %}
|
|
||||||
{% if alreadyExisting %}
|
|
||||||
<p class="failed">Cette personne est déjà dans l'équipe</p>
|
|
||||||
{% endif %}
|
|
||||||
<input type="text" id="email" name="email" required>
|
|
||||||
|
|
||||||
<fieldset class="role">
|
|
||||||
<legend>Rôle du membre dans l'équipe :</legend>
|
|
||||||
<div class="radio">
|
|
||||||
<label for="P">Joueur</label>
|
|
||||||
<input type="radio" id="P" name="role" value="PLAYER" checked />
|
|
||||||
</div>
|
|
||||||
<div class="radio">
|
|
||||||
<label for="C">Coach</label>
|
|
||||||
<input type="radio" id="C" name="role" value="COACH" />
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="submit" value="Confirmer">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,73 +0,0 @@
|
|||||||
<!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>
|
|
@ -1,46 +0,0 @@
|
|||||||
<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>
|
|
@ -1,107 +0,0 @@
|
|||||||
<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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.error-messages {
|
|
||||||
color: #ff331a;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
{% for err in fails %}
|
|
||||||
.form-group
|
|
||||||
|
|
||||||
#
|
|
||||||
{{ err.getFieldName() }}
|
|
||||||
{
|
|
||||||
border-color: red
|
|
||||||
;
|
|
||||||
}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
.inscr {
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
|
|
||||||
#buttons{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 10px 20px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.button{
|
|
||||||
background-color: #007bff;
|
|
||||||
color: #fff;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover{
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
</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.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>
|
|
||||||
<a href="{{ path('/register') }}" class="inscr">Vous n'avez pas de compte ?</a>
|
|
||||||
<br><br>
|
|
||||||
<div id = "buttons">
|
|
||||||
<input class = "button" type="submit" value="Se connecter">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,116 +0,0 @@
|
|||||||
<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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-messages {
|
|
||||||
color: #ff331a;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{% for err in fails %}
|
|
||||||
.form-group
|
|
||||||
|
|
||||||
#
|
|
||||||
{{ err.getFieldName() }}
|
|
||||||
{
|
|
||||||
border-color: red
|
|
||||||
;
|
|
||||||
}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
.inscr{
|
|
||||||
font-size: small;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#buttons{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 10px 20px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.button{
|
|
||||||
background-color: #007bff;
|
|
||||||
color: #fff;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover{
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</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>
|
|
||||||
<a href="{{ path('/login') }}" class="inscr">Vous avez déjà un compte ?</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div id = "buttons">
|
|
||||||
<input class = "button" type="submit" value="Créer votre compte">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,18 +0,0 @@
|
|||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Twig view</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>Hello world</h1>
|
|
||||||
|
|
||||||
|
|
||||||
{% for v in results %}
|
|
||||||
<p>username: {{ v.name }}</p>
|
|
||||||
<p>description: {{ v.description }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,109 +0,0 @@
|
|||||||
<!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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
background-color: #fff;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#colors{
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
.color {
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 80px;
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#delete{
|
|
||||||
border-radius:10px ;
|
|
||||||
background-color: red;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player{
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1><a href="{{ path('/') }}">IQBall</a></h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="container">
|
|
||||||
{% if notDeleted %}
|
|
||||||
<popup>
|
|
||||||
<p>Cette équipe ne peut être supprimée.</p>
|
|
||||||
</popup>
|
|
||||||
{% endif %}
|
|
||||||
{% if team is defined %}
|
|
||||||
<div class="team">
|
|
||||||
<div>
|
|
||||||
<h1>{{ team.getInfo().getName() }}</h1>
|
|
||||||
<img src="{{ team.getInfo().getPicture() }}" alt="Logo d'équipe" class="logo">
|
|
||||||
</div>
|
|
||||||
<div id="colors">
|
|
||||||
<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>
|
|
||||||
{% if isCoach %}
|
|
||||||
<button id="delete" onclick="confirm('Êtes-vous sûr de supprimer cette équipe?') ? window.location.href = '{{ path("/team/#{team.getInfo().getId()}/delete") }}' : {}">Supprimer</button>
|
|
||||||
<button></button>
|
|
||||||
{% endif %}
|
|
||||||
{% for m in team.listMembers() %}
|
|
||||||
<div class="player">
|
|
||||||
<p> {{ m.getUserId() }} </p>
|
|
||||||
{% if m.getRole().isCoach() %}
|
|
||||||
<p> : Coach</p>
|
|
||||||
{% else %}
|
|
||||||
<p> : Joueur</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div>
|
|
||||||
<h3>Cette équipe ne peut être affichée</h3>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,61 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Twig view</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
section{
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-around;
|
|
||||||
background-color: white;
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.team {
|
|
||||||
border-radius: 10px;
|
|
||||||
border-color: darkgrey;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo_team {
|
|
||||||
width: 15%;
|
|
||||||
aspect-ratio: 3/2;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1><a href="{{ path('/') }}">IQBall</a></h1>
|
|
||||||
</header>
|
|
||||||
<section>
|
|
||||||
{% if teams is empty %}
|
|
||||||
<p>Aucune équipe n'a été trouvée</p>
|
|
||||||
<div class="container">
|
|
||||||
<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.getId()}") }}'">
|
|
||||||
<p>Nom de l'équipe : {{ t.getName() }}</p>
|
|
||||||
<img src="{{ t.getPicture() }}" alt="logo de l'équipe" class="logo_team">
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,81 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Insertion view</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 5px auto;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
{% for item in bad_fields %}
|
|
||||||
#{{ item }}{
|
|
||||||
border-color: red;
|
|
||||||
}{% endfor %} input[type="text"], input[type="password"] {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="submit"] {
|
|
||||||
background-color: #007bff;
|
|
||||||
color: #fff;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="submit"]:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<h2>Modifier votre équipe</h2>
|
|
||||||
<form action="{{ path('/team/' ~ team.getInfo().getId() ~ '/edit') }}" method="post">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="name">Nom de l'équipe :</label>
|
|
||||||
<input type="text" id="name" name="name" value="{{ team.getInfo().getName() }}" required>
|
|
||||||
<label for="picture">Logo:</label>
|
|
||||||
<input type="text" id="picture" name="picture" value="{{ team.getInfo().getPicture() }}" required>
|
|
||||||
<label for="main_color">Couleur principale</label>
|
|
||||||
<input type="color" value="{{ team.getInfo().getMainColor() }}" id="main_color" name="main_color" required>
|
|
||||||
<label for="second_color">Couleur secondaire</label>
|
|
||||||
<input type="color" id="second_color" name="second_color" value="{{ team.getInfo().getSecondColor() }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="submit" value="Confirmer">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,57 +0,0 @@
|
|||||||
<!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>
|
|
@ -1,97 +0,0 @@
|
|||||||
<!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>
|
|
||||||
<button onclick="location.pathname='{{ path('/disconnect') }}'"> Se déconnecter</button>
|
|
||||||
<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éée !</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éée !</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,81 +0,0 @@
|
|||||||
<!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" value="#ffffff" id="main_color" name="main_color" required>
|
|
||||||
<label for="second_color">Couleur secondaire</label>
|
|
||||||
<input type="color" id="second_color" name="second_color" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="submit" value="Confirmer">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,79 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Insertion view</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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>
|
|
||||||
<header>
|
|
||||||
<h1><a href="{{ path('/') }}">IQBall</a></h1>
|
|
||||||
</header>
|
|
||||||
<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,58 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<script type="module">
|
|
||||||
<?php
|
|
||||||
if (SUPPORTS_FAST_REFRESH) {
|
|
||||||
$asset_server = asset("");
|
|
||||||
echo "
|
|
||||||
import RefreshRuntime from '{$asset_server}front/@react-refresh'
|
|
||||||
RefreshRuntime.injectIntoGlobalHook(window)
|
|
||||||
window.\$RefreshReg$ = () => {}
|
|
||||||
window.\$RefreshSig$ = () => (type) => type
|
|
||||||
window.__vite_plugin_react_preamble_installed__ = true
|
|
||||||
";
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<link rel="icon" href="<?= asset("assets/favicon.ico") ?>">
|
|
||||||
|
|
||||||
<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>Document</title>
|
|
||||||
|
|
||||||
<!-- remove default screen margin,
|
|
||||||
html and body to take full screen size -->
|
|
||||||
<style>
|
|
||||||
body, html, #root {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="root"></div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
here's the magic.
|
|
||||||
imports the given view URL, and assume that the view exports a function named `Component`.
|
|
||||||
see ViewRenderer.tsx::renderView for more info
|
|
||||||
-->
|
|
||||||
<script type="module">
|
|
||||||
import {renderView} from "<?= asset("ViewRenderer.tsx") ?>"
|
|
||||||
import Component from "<?= asset($url) ?>"
|
|
||||||
renderView(Component, <?= json_encode($arguments) ?>)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,13 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* sends a react view to the user client.
|
|
||||||
* @param string $url url of the react file to render
|
|
||||||
* @param array<string, mixed> $arguments arguments to pass to the rendered react component
|
|
||||||
* The arguments must be a json-encodable key/value dictionary.
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function send_react_front(string $url, array $arguments) {
|
|
||||||
// the $url and $argument values are used into the included file
|
|
||||||
require_once "react-display-file.php";
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
<?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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
<?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 string token
|
|
||||||
*/
|
|
||||||
private string $token;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var User contains all the account's "public" information
|
|
||||||
*/
|
|
||||||
private User $user;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $token
|
|
||||||
* @param User $user
|
|
||||||
*/
|
|
||||||
public function __construct(string $token, User $user) {
|
|
||||||
$this->token = $token;
|
|
||||||
$this->user = $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getToken(): string {
|
|
||||||
return $this->token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return User
|
|
||||||
*/
|
|
||||||
public function getUser(): User {
|
|
||||||
return $this->user;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Data;
|
|
||||||
|
|
||||||
use InvalidArgumentException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enumeration class workaround
|
|
||||||
* As there is no enumerations in php 7.4, this class
|
|
||||||
* encapsulates an integer value and use it as a variant discriminant
|
|
||||||
*/
|
|
||||||
final class CourtType {
|
|
||||||
private const COURT_PLAIN = 0;
|
|
||||||
private const COURT_HALF = 1;
|
|
||||||
private int $value;
|
|
||||||
|
|
||||||
private function __construct(int $val) {
|
|
||||||
if ($val < self::COURT_PLAIN || $val > self::COURT_HALF) {
|
|
||||||
throw new InvalidArgumentException("Valeur du rôle invalide");
|
|
||||||
}
|
|
||||||
$this->value = $val;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function plain(): CourtType {
|
|
||||||
return new CourtType(CourtType::COURT_PLAIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function half(): CourtType {
|
|
||||||
return new CourtType(CourtType::COURT_HALF);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function name(): string {
|
|
||||||
switch ($this->value) {
|
|
||||||
case self::COURT_HALF:
|
|
||||||
return "HALF";
|
|
||||||
case self::COURT_PLAIN:
|
|
||||||
return "PLAIN";
|
|
||||||
}
|
|
||||||
die("unreachable");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromName(string $name): ?CourtType {
|
|
||||||
switch ($name) {
|
|
||||||
case "HALF":
|
|
||||||
return CourtType::half();
|
|
||||||
case "PLAIN":
|
|
||||||
return CourtType::plain();
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isPlain(): bool {
|
|
||||||
return ($this->value == self::COURT_PLAIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isHalf(): bool {
|
|
||||||
return ($this->value == self::COURT_HALF);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* information about a team member
|
|
||||||
*/
|
|
||||||
class Member implements \JsonSerializable {
|
|
||||||
private User $user;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var int The member's team id
|
|
||||||
*/
|
|
||||||
private int $teamId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string the member's role
|
|
||||||
*/
|
|
||||||
private string $role;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param User $user
|
|
||||||
* @param int $teamId
|
|
||||||
* @param string $role
|
|
||||||
*/
|
|
||||||
public function __construct(User $user, int $teamId, string $role) {
|
|
||||||
$this->user = $user;
|
|
||||||
$this->teamId = $teamId;
|
|
||||||
$this->role = $role;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getRole(): string {
|
|
||||||
return $this->role;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getTeamId(): int {
|
|
||||||
return $this->teamId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return User
|
|
||||||
*/
|
|
||||||
public function getUser(): User {
|
|
||||||
return $this->user;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function jsonSerialize() {
|
|
||||||
return get_object_vars($this);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Data;
|
|
||||||
|
|
||||||
class TacticInfo {
|
|
||||||
private int $id;
|
|
||||||
private string $name;
|
|
||||||
private int $creationDate;
|
|
||||||
private int $ownerId;
|
|
||||||
private CourtType $courtType;
|
|
||||||
private string $content;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $id
|
|
||||||
* @param string $name
|
|
||||||
* @param int $creationDate
|
|
||||||
* @param int $ownerId
|
|
||||||
* @param CourtType $type
|
|
||||||
* @param string $content
|
|
||||||
*/
|
|
||||||
public function __construct(int $id, string $name, int $creationDate, int $ownerId, CourtType $type, string $content) {
|
|
||||||
$this->id = $id;
|
|
||||||
$this->name = $name;
|
|
||||||
$this->ownerId = $ownerId;
|
|
||||||
$this->creationDate = $creationDate;
|
|
||||||
$this->courtType = $type;
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCourtType(): CourtType {
|
|
||||||
return $this->courtType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getCreationDate(): int {
|
|
||||||
return $this->creationDate;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Data;
|
|
||||||
|
|
||||||
class Team implements \JsonSerializable {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function jsonSerialize() {
|
|
||||||
return get_object_vars($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Data;
|
|
||||||
|
|
||||||
class TeamInfo implements \JsonSerializable {
|
|
||||||
private int $id;
|
|
||||||
private string $name;
|
|
||||||
private string $picture;
|
|
||||||
private string $mainColor;
|
|
||||||
private string $secondColor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $id
|
|
||||||
* @param string $name
|
|
||||||
* @param string $picture
|
|
||||||
* @param string $mainColor
|
|
||||||
* @param string $secondColor
|
|
||||||
*/
|
|
||||||
public function __construct(int $id, string $name, string $picture, string $mainColor, string $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(): string {
|
|
||||||
return $this->mainColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSecondColor(): string {
|
|
||||||
return $this->secondColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function jsonSerialize() {
|
|
||||||
return get_object_vars($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Data;
|
|
||||||
|
|
||||||
use _PHPStan_4c4f22f13\Nette\Utils\Json;
|
|
||||||
|
|
||||||
class User implements \JsonSerializable {
|
|
||||||
/**
|
|
||||||
* @var string $email user's mail address
|
|
||||||
*/
|
|
||||||
private string $email;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string the user's username
|
|
||||||
*/
|
|
||||||
private string $name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var int the user's id
|
|
||||||
*/
|
|
||||||
private int $id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string user's profile picture
|
|
||||||
*/
|
|
||||||
private string $profilePicture;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $email
|
|
||||||
* @param string $name
|
|
||||||
* @param int $id
|
|
||||||
* @param string $profilePicture
|
|
||||||
*/
|
|
||||||
public function __construct(string $email, string $name, int $id, string $profilePicture) {
|
|
||||||
$this->email = $email;
|
|
||||||
$this->name = $name;
|
|
||||||
$this->id = $id;
|
|
||||||
$this->profilePicture = $profilePicture;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getEmail(): string {
|
|
||||||
return $this->email;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getName(): string {
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getId(): int {
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getProfilePicture(): string {
|
|
||||||
return $this->profilePicture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function jsonSerialize() {
|
|
||||||
return get_object_vars($this);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Gateway;
|
|
||||||
|
|
||||||
use IQBall\Core\Connection;
|
|
||||||
use IQBall\Core\Data\Member;
|
|
||||||
use IQBall\Core\Data\User;
|
|
||||||
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,a.email,a.username,a.profilePicture,m.role FROM Account a,team t,Member m WHERE t.id = :id AND m.id_team = t.id AND m.id_user = a.id",
|
|
||||||
[
|
|
||||||
":id" => [$teamId, PDO::PARAM_INT],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
return array_map(fn($row) => new Member(new User($row['email'], $row['username'], $row['id'], $row['profilePicture']), $teamId, $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],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $email
|
|
||||||
* @param int $idTeam
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isCoach(string $email, int $idTeam): bool {
|
|
||||||
$result = $this->con->fetch(
|
|
||||||
"SELECT role FROM Member WHERE id_team=:team AND id_user = (SELECT id FROM Account WHERE email=:email)",
|
|
||||||
[
|
|
||||||
"team" => [$idTeam, PDO::PARAM_INT],
|
|
||||||
"email" => [$email, PDO::PARAM_STR],
|
|
||||||
]
|
|
||||||
)[0]['role'];
|
|
||||||
|
|
||||||
return $result == 'COACH';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $idTeam
|
|
||||||
* @param int $idCurrentUser
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isMemberOfTeam(int $idTeam, int $idCurrentUser): bool {
|
|
||||||
$result = $this->con->fetch(
|
|
||||||
"SELECT id_user FROM Member WHERE id_team = :team AND id_user = :user",
|
|
||||||
[
|
|
||||||
"team" => [$idTeam, PDO::PARAM_INT],
|
|
||||||
"user" => [$idCurrentUser, PDO::PARAM_INT],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
return !empty($result);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Gateway;
|
|
||||||
|
|
||||||
use IQBall\Core\Connection;
|
|
||||||
use IQBall\Core\Data\CourtType;
|
|
||||||
use IQBall\Core\Data\TacticInfo;
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
class TacticInfoGateway {
|
|
||||||
private Connection $con;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Connection $con
|
|
||||||
*/
|
|
||||||
public function __construct(Connection $con) {
|
|
||||||
$this->con = $con;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get tactic information from given identifier
|
|
||||||
* @param int $id
|
|
||||||
* @return TacticInfo|null
|
|
||||||
*/
|
|
||||||
public function get(int $id): ?TacticInfo {
|
|
||||||
$res = $this->con->fetch(
|
|
||||||
"SELECT * FROM Tactic WHERE id = :id",
|
|
||||||
[":id" => [$id, PDO::PARAM_INT]]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isset($res[0])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$row = $res[0];
|
|
||||||
|
|
||||||
$type = CourtType::fromName($row['court_type']);
|
|
||||||
return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]), $row["owner"], $type, $row['content']);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the nb last tactics created
|
|
||||||
*
|
|
||||||
* @param integer $nb
|
|
||||||
* @return array<array<string,mixed>>
|
|
||||||
*/
|
|
||||||
public function getLast(int $nb, int $ownerId): ?array {
|
|
||||||
$res = $this->con->fetch(
|
|
||||||
"SELECT *
|
|
||||||
FROM Tactic
|
|
||||||
WHERE owner = :ownerId
|
|
||||||
ORDER BY creation_date DESC
|
|
||||||
LIMIT :nb",
|
|
||||||
[
|
|
||||||
":ownerId" => [$ownerId, PDO::PARAM_INT],":nb" => [$nb, PDO::PARAM_INT],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
if (count($res) == 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return $res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the tactics of the owner
|
|
||||||
*
|
|
||||||
* @return array<array<string,mixed>>
|
|
||||||
*/
|
|
||||||
public function getAll(int $ownerId): ?array {
|
|
||||||
$res = $this->con->fetch(
|
|
||||||
"SELECT *
|
|
||||||
FROM Tactic
|
|
||||||
WHERE owner = :ownerId
|
|
||||||
ORDER BY name DESC",
|
|
||||||
[
|
|
||||||
":ownerId" => [$ownerId, PDO::PARAM_INT],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
if (count($res) == 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return $res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @param int $owner
|
|
||||||
* @param CourtType $type
|
|
||||||
* @return int inserted tactic id
|
|
||||||
*/
|
|
||||||
public function insert(string $name, int $owner, CourtType $type): int {
|
|
||||||
$this->con->exec(
|
|
||||||
"INSERT INTO Tactic(name, owner, court_type) VALUES(:name, :owner, :court_type)",
|
|
||||||
[
|
|
||||||
":name" => [$name, PDO::PARAM_STR],
|
|
||||||
":owner" => [$owner, PDO::PARAM_INT],
|
|
||||||
":court_type" => [$type->name(), PDO::PARAM_STR],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
return intval($this->con->lastInsertId());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update name of given tactic identifier
|
|
||||||
* @param int $id
|
|
||||||
* @param string $name
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function updateName(int $id, string $name): bool {
|
|
||||||
$stmnt = $this->con->prepare("UPDATE Tactic SET name = :name WHERE id = :id");
|
|
||||||
$stmnt->execute([
|
|
||||||
":name" => $name,
|
|
||||||
":id" => $id,
|
|
||||||
]);
|
|
||||||
return $stmnt->rowCount() == 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/***
|
|
||||||
* Updates a given tactics content
|
|
||||||
* @param int $id
|
|
||||||
* @param string $json
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function updateContent(int $id, string $json): bool {
|
|
||||||
$stmnt = $this->con->prepare("UPDATE Tactic SET content = :content WHERE id = :id");
|
|
||||||
$stmnt->execute([
|
|
||||||
":content" => $json,
|
|
||||||
":id" => $id,
|
|
||||||
]);
|
|
||||||
return $stmnt->rowCount() == 1;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Http;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class to define constants of used http codes
|
|
||||||
*/
|
|
||||||
class HttpCodes {
|
|
||||||
public const OK = 200;
|
|
||||||
public const FOUND = 302;
|
|
||||||
public const BAD_REQUEST = 400;
|
|
||||||
public const UNAUTHORIZED = 401;
|
|
||||||
|
|
||||||
public const FORBIDDEN = 403;
|
|
||||||
|
|
||||||
public const NOT_FOUND = 404;
|
|
||||||
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Http;
|
|
||||||
|
|
||||||
use IQBall\Core\Validation\FieldValidationFail;
|
|
||||||
use IQBall\Core\Validation\Validation;
|
|
||||||
use IQBall\Core\Validation\ValidationFail;
|
|
||||||
use IQBall\Core\Validation\Validator;
|
|
||||||
use ArrayAccess;
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements ArrayAccess<string, mixed>
|
|
||||||
* */
|
|
||||||
class HttpRequest implements ArrayAccess {
|
|
||||||
/**
|
|
||||||
* @var array<string, mixed>
|
|
||||||
*/
|
|
||||||
private array $data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $data
|
|
||||||
*/
|
|
||||||
private function __construct(array $data) {
|
|
||||||
$this->data = $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new HttpRequest instance, and ensures that the given request data validates the given schema.
|
|
||||||
* This is a simple function that only supports flat schemas (non-composed, the data must only be a k/v array pair.)
|
|
||||||
* @param array<string, mixed> $request the request's data
|
|
||||||
* @param array<string, ValidationFail> $fails a reference to a failure array, that will contain the reported validation failures.
|
|
||||||
* @param array<string, Validator[]> $schema the schema to satisfy. a schema is a simple array with a string key (which is the top-level field name), and a set of validators
|
|
||||||
* @return HttpRequest|null the built HttpRequest instance, or null if a field is missing, or if any of the schema validator failed
|
|
||||||
*/
|
|
||||||
public static function from(array $request, array &$fails, array $schema): ?HttpRequest {
|
|
||||||
$failure = false;
|
|
||||||
foreach ($schema as $fieldName => $fieldValidators) {
|
|
||||||
if (!isset($request[$fieldName])) {
|
|
||||||
$fails[] = FieldValidationFail::missing($fieldName);
|
|
||||||
$failure = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$failure |= Validation::validate($request[$fieldName], $fieldName, $fails, ...$fieldValidators);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($failure) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new HttpRequest($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function offsetExists($offset): bool {
|
|
||||||
return isset($this->data[$offset]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $offset
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function offsetGet($offset) {
|
|
||||||
return $this->data[$offset];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $offset
|
|
||||||
* @param $value
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function offsetSet($offset, $value) {
|
|
||||||
throw new Exception("requests are immutable objects.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $offset
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function offsetUnset($offset) {
|
|
||||||
throw new Exception("requests are immutable objects.");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Http;
|
|
||||||
|
|
||||||
class HttpResponse {
|
|
||||||
/**
|
|
||||||
* @var array<string, string>
|
|
||||||
*/
|
|
||||||
private array $headers;
|
|
||||||
private int $code;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $code
|
|
||||||
* @param array<string, string> $headers
|
|
||||||
*/
|
|
||||||
public function __construct(int $code, array $headers) {
|
|
||||||
$this->code = $code;
|
|
||||||
$this->headers = $headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCode(): int {
|
|
||||||
return $this->code;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public function getHeaders(): array {
|
|
||||||
return $this->headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $code
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public static function fromCode(int $code): HttpResponse {
|
|
||||||
return new HttpResponse($code, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $url the url to redirect
|
|
||||||
* @param int $code only HTTP 3XX codes are accepted.
|
|
||||||
* @return HttpResponse a response that will redirect client to given url
|
|
||||||
*/
|
|
||||||
|
|
||||||
public static function redirect(string $url, int $code = HttpCodes::FOUND): HttpResponse {
|
|
||||||
global $basePath;
|
|
||||||
return self::redirect_absolute($basePath . $url, $code);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $url the url to redirect
|
|
||||||
* @param int $code only HTTP 3XX codes are accepted.
|
|
||||||
* @return HttpResponse a response that will redirect client to given url
|
|
||||||
*/
|
|
||||||
|
|
||||||
public static function redirect_absolute(string $url, int $code = HttpCodes::FOUND): HttpResponse {
|
|
||||||
if ($code < 300 || $code >= 400) {
|
|
||||||
throw new \InvalidArgumentException("given code is not a redirection http code");
|
|
||||||
}
|
|
||||||
return new HttpResponse($code, ["Location" => $url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Http;
|
|
||||||
|
|
||||||
class JsonHttpResponse extends HttpResponse {
|
|
||||||
/**
|
|
||||||
* @var mixed Any JSON serializable value
|
|
||||||
*/
|
|
||||||
private $payload;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mixed $payload
|
|
||||||
*/
|
|
||||||
public function __construct($payload, int $code = HttpCodes::OK) {
|
|
||||||
parent::__construct($code, []);
|
|
||||||
$this->payload = $payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getJson(): string {
|
|
||||||
$result = json_encode($this->payload);
|
|
||||||
if (!$result) {
|
|
||||||
throw new \RuntimeException("Given payload is not json encodable");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Model;
|
|
||||||
|
|
||||||
use IQBall\Core\Data\CourtType;
|
|
||||||
use IQBall\App\Session\SessionHandle;
|
|
||||||
use IQBall\Core\Data\TacticInfo;
|
|
||||||
use IQBall\Core\Gateway\TacticInfoGateway;
|
|
||||||
use IQBall\Core\Validation\ValidationFail;
|
|
||||||
|
|
||||||
class TacticModel {
|
|
||||||
public const TACTIC_DEFAULT_NAME = "Nouvelle tactique";
|
|
||||||
|
|
||||||
|
|
||||||
private TacticInfoGateway $tactics;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TacticInfoGateway $tactics
|
|
||||||
*/
|
|
||||||
public function __construct(TacticInfoGateway $tactics) {
|
|
||||||
$this->tactics = $tactics;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates a new empty tactic, with given name
|
|
||||||
* @param string $name
|
|
||||||
* @param int $ownerId
|
|
||||||
* @param CourtType $type
|
|
||||||
* @return TacticInfo
|
|
||||||
*/
|
|
||||||
public function makeNew(string $name, int $ownerId, CourtType $type): TacticInfo {
|
|
||||||
$id = $this->tactics->insert($name, $ownerId, $type);
|
|
||||||
return $this->tactics->get($id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates a new empty tactic, with a default name
|
|
||||||
* @param int $ownerId
|
|
||||||
* @param CourtType $type
|
|
||||||
* @return TacticInfo|null
|
|
||||||
*/
|
|
||||||
public function makeNewDefault(int $ownerId, CourtType $type): ?TacticInfo {
|
|
||||||
return $this->makeNew(self::TACTIC_DEFAULT_NAME, $ownerId, $type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tries to retrieve information about a tactic
|
|
||||||
* @param int $id tactic identifier
|
|
||||||
* @return TacticInfo|null or null if the identifier did not match a tactic
|
|
||||||
*/
|
|
||||||
public function get(int $id): ?TacticInfo {
|
|
||||||
return $this->tactics->get($id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the nb last tactics created
|
|
||||||
*
|
|
||||||
* @param integer $nb
|
|
||||||
* @return array<array<string,mixed>>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the nb last tactics
|
|
||||||
*
|
|
||||||
* @param integer $nb
|
|
||||||
* @param integer $ownerId
|
|
||||||
* @return array<array<string,mixed>>
|
|
||||||
*/
|
|
||||||
public function getLast(int $nb, int $ownerId): array {
|
|
||||||
return $this->tactics->getLast($nb, $ownerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the tactics of the owner
|
|
||||||
*
|
|
||||||
* @param integer $ownerId
|
|
||||||
* @return array<array<string,mixed>>
|
|
||||||
*/
|
|
||||||
public function getAll(int $ownerId): ?array {
|
|
||||||
return $this->tactics->getAll($ownerId);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Update the name of a tactic
|
|
||||||
* @param int $id the tactic identifier
|
|
||||||
* @param string $name the new name to set
|
|
||||||
* @return ValidationFail[] failures, if any
|
|
||||||
*/
|
|
||||||
public function updateName(int $id, string $name, int $authId): array {
|
|
||||||
|
|
||||||
$tactic = $this->tactics->get($id);
|
|
||||||
|
|
||||||
if ($tactic == null) {
|
|
||||||
return [ValidationFail::notFound("Could not find tactic")];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($tactic->getOwnerId() != $authId) {
|
|
||||||
return [ValidationFail::unauthorized()];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->tactics->updateName($id, $name)) {
|
|
||||||
return [ValidationFail::error("Could not update name")];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateContent(int $id, string $json): ?ValidationFail {
|
|
||||||
if (!$this->tactics->updateContent($id, $json)) {
|
|
||||||
return ValidationFail::error("Could not update content");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,142 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Model;
|
|
||||||
|
|
||||||
use IQBall\Core\Data\Team;
|
|
||||||
use IQBall\Core\Data\TeamInfo;
|
|
||||||
use IQBall\Core\Gateway\AccountGateway;
|
|
||||||
use IQBall\Core\Gateway\MemberGateway;
|
|
||||||
use IQBall\Core\Gateway\TeamGateway;
|
|
||||||
|
|
||||||
class TeamModel {
|
|
||||||
private AccountGateway $users;
|
|
||||||
private TeamGateway $teams;
|
|
||||||
private MemberGateway $members;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TeamGateway $gateway
|
|
||||||
* @param MemberGateway $members
|
|
||||||
* @param AccountGateway $users
|
|
||||||
*/
|
|
||||||
public function __construct(TeamGateway $gateway, MemberGateway $members, AccountGateway $users) {
|
|
||||||
$this->teams = $gateway;
|
|
||||||
$this->members = $members;
|
|
||||||
$this->users = $users;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a team
|
|
||||||
* @param string $name
|
|
||||||
* @param string $picture
|
|
||||||
* @param string $mainColor
|
|
||||||
* @param string $secondColor
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function createTeam(string $name, string $picture, string $mainColor, string $secondColor): int {
|
|
||||||
return $this->teams->insert($name, $picture, $mainColor, $secondColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add a member to a team
|
|
||||||
* @param string $mail
|
|
||||||
* @param int $teamId
|
|
||||||
* @param string $role
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function addMember(string $mail, int $teamId, string $role): int {
|
|
||||||
$user = $this->users->getAccountFromMail($mail);
|
|
||||||
if($user == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if(!$this->members->isMemberOfTeam($teamId, $user->getUser()->getId())) {
|
|
||||||
$this->members->insert($teamId, $user->getUser()->getId(), $role);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @param int $id
|
|
||||||
* @return TeamInfo[]
|
|
||||||
*/
|
|
||||||
public function listByName(string $name, int $id): array {
|
|
||||||
return $this->teams->listByName($name, $id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $idTeam
|
|
||||||
* @param int $idCurrentUser
|
|
||||||
* @return Team|null
|
|
||||||
*/
|
|
||||||
public function getTeam(int $idTeam, int $idCurrentUser): ?Team {
|
|
||||||
if(!$this->members->isMemberOfTeam($idTeam, $idCurrentUser)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$teamInfo = $this->teams->getTeamById($idTeam);
|
|
||||||
$members = $this->members->getMembersOfTeam($idTeam);
|
|
||||||
return new Team($teamInfo, $members);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* delete a member from given team identifier
|
|
||||||
* @param int $idMember
|
|
||||||
* @param int $teamId
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function deleteMember(int $idMember, int $teamId): int {
|
|
||||||
$this->members->remove($teamId, $idMember);
|
|
||||||
if(empty($this->members->getMembersOfTeam($teamId))) {
|
|
||||||
$this->teams->deleteTeam($teamId);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return $teamId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a team
|
|
||||||
* @param string $email
|
|
||||||
* @param int $idTeam
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function deleteTeam(string $email, int $idTeam): int {
|
|
||||||
if($this->members->isCoach($email, $idTeam)) {
|
|
||||||
$this->teams->deleteTeam($idTeam);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify if the account associated to an email is in a specific team indicated with its id
|
|
||||||
* @param int $idTeam
|
|
||||||
* @param string $email
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isCoach(int $idTeam, string $email): bool {
|
|
||||||
return $this->members->isCoach($email, $idTeam);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edit a team with its id, and replace the current attributes with the new ones
|
|
||||||
* @param int $idTeam
|
|
||||||
* @param string $newName
|
|
||||||
* @param string $newPicture
|
|
||||||
* @param string $newMainColor
|
|
||||||
* @param string $newSecondColor
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function editTeam(int $idTeam, string $newName, string $newPicture, string $newMainColor, string $newSecondColor) {
|
|
||||||
$this->teams->editTeam($idTeam, $newName, $newPicture, $newMainColor, $newSecondColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all user's teams
|
|
||||||
*
|
|
||||||
* @param integer $user
|
|
||||||
* @return array<array<string, mixed>>
|
|
||||||
*/
|
|
||||||
public function getAll(int $user): array {
|
|
||||||
return $this->teams->getAll($user);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Validation;
|
|
||||||
|
|
||||||
class ComposedValidator extends Validator {
|
|
||||||
private Validator $first;
|
|
||||||
private Validator $then;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Validator $first
|
|
||||||
* @param Validator $then
|
|
||||||
*/
|
|
||||||
public function __construct(Validator $first, Validator $then) {
|
|
||||||
$this->first = $first;
|
|
||||||
$this->then = $then;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function validate(string $name, $val): array {
|
|
||||||
$firstFailures = $this->first->validate($name, $val);
|
|
||||||
$thenFailures = [];
|
|
||||||
if (empty($firstFailures)) {
|
|
||||||
$thenFailures = $this->then->validate($name, $val);
|
|
||||||
}
|
|
||||||
return array_merge($firstFailures, $thenFailures);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Validation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An error that concerns a field, with a bound message name
|
|
||||||
*/
|
|
||||||
class FieldValidationFail extends ValidationFail {
|
|
||||||
private string $fieldName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $fieldName
|
|
||||||
* @param string $message
|
|
||||||
*/
|
|
||||||
public function __construct(string $fieldName, string $message) {
|
|
||||||
parent::__construct("Champ invalide", $message);
|
|
||||||
$this->fieldName = $fieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFieldName(): string {
|
|
||||||
return $this->fieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function invalidChars(string $fieldName): FieldValidationFail {
|
|
||||||
return new FieldValidationFail($fieldName, "field contains illegal chars");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function empty(string $fieldName): FieldValidationFail {
|
|
||||||
return new FieldValidationFail($fieldName, "field is empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function missing(string $fieldName): FieldValidationFail {
|
|
||||||
return new FieldValidationFail($fieldName, "field is missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public function jsonSerialize(): array {
|
|
||||||
return ["field" => $this->fieldName, "message" => $this->getMessage()];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Validation;
|
|
||||||
|
|
||||||
class FunctionValidator extends Validator {
|
|
||||||
/**
|
|
||||||
* @var callable(string, mixed): ValidationFail[]
|
|
||||||
*/
|
|
||||||
private $validate_fn;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callable(string, mixed): ValidationFail[] $validate_fn the validate function. Must have the same signature as the {@link Validator::validate()} method.
|
|
||||||
*/
|
|
||||||
public function __construct(callable $validate_fn) {
|
|
||||||
$this->validate_fn = $validate_fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function validate(string $name, $val): array {
|
|
||||||
return call_user_func_array($this->validate_fn, [$name, $val]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Validation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple validator that takes a predicate and an error factory
|
|
||||||
*/
|
|
||||||
class SimpleFunctionValidator extends Validator {
|
|
||||||
/**
|
|
||||||
* @var callable(mixed): bool
|
|
||||||
*/
|
|
||||||
private $predicate;
|
|
||||||
/**
|
|
||||||
* @var callable(string): ValidationFail[]
|
|
||||||
*/
|
|
||||||
private $errorFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callable(mixed): bool $predicate a function predicate with signature: `(string) => bool`, to validate the given string
|
|
||||||
* @param callable(string): ValidationFail[] $errorsFactory a factory function with signature `(string) => array` to emit failures when the predicate fails
|
|
||||||
*/
|
|
||||||
public function __construct(callable $predicate, callable $errorsFactory) {
|
|
||||||
$this->predicate = $predicate;
|
|
||||||
$this->errorFactory = $errorsFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function validate(string $name, $val): array {
|
|
||||||
if (!call_user_func_array($this->predicate, [$val])) {
|
|
||||||
return call_user_func_array($this->errorFactory, [$name]);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|