Compare commits
86 Commits
shareTacti
...
master
Author | SHA1 | Date |
---|---|---|
|
b941d6530c | 1 year ago |
|
1faa8168f9 | 1 year ago |
|
0de42db300 | 1 year ago |
![]() |
262cf97445 | 1 year ago |
![]() |
47d81bb665 | 1 year ago |
![]() |
eef1e16830 | 1 year ago |
![]() |
98eed72af6 | 1 year ago |
![]() |
7289a956b3 | 1 year ago |
|
3da28f828d | 1 year ago |
![]() |
f9e436ea12 | 1 year ago |
![]() |
cab6fc43ca | 1 year ago |
![]() |
3091e1a61a | 1 year ago |
![]() |
15b47f354e | 1 year ago |
|
9e8606184c | 1 year ago |
![]() |
f9c42862e0 | 1 year ago |
![]() |
d6f1a633a1 | 1 year ago |
![]() |
42c0300ced | 1 year ago |
![]() |
fa7339b0f1 | 1 year ago |
![]() |
6738ddcb67 | 1 year ago |
![]() |
62be8f2a0b | 1 year ago |
![]() |
3a983f593a | 1 year ago |
|
307a978e15 | 1 year ago |
![]() |
bef196e09e | 1 year ago |
![]() |
882e45f328 | 1 year ago |
![]() |
69edb10b04 | 1 year ago |
![]() |
80c94d733f | 1 year ago |
![]() |
c544f88de7 | 1 year ago |
![]() |
0eba18784e | 1 year ago |
|
4ea531c08c | 1 year ago |
![]() |
102bc774af | 1 year ago |
![]() |
415350887d | 1 year ago |
|
cca7ee1b1b | 1 year ago |
![]() |
b87db24e9e | 1 year ago |
![]() |
f7e2b9216d | 1 year ago |
|
df10eba8d2 | 1 year ago |
![]() |
4fe1ddfbd2 | 1 year ago |
![]() |
fcd0a94535 | 1 year ago |
![]() |
64e8362e53 | 1 year ago |
![]() |
32b79ed5c4 | 1 year ago |
![]() |
2577974bfe | 1 year ago |
![]() |
f5b7b61411 | 1 year ago |
|
72273e3f3e | 1 year ago |
![]() |
4cee7649af | 1 year ago |
![]() |
1d235da809 | 1 year ago |
![]() |
6b4e5ba36b | 1 year ago |
![]() |
65672c404a | 1 year ago |
![]() |
6756e4064e | 1 year ago |
![]() |
d26edd791a | 1 year ago |
![]() |
034afc3649 | 1 year ago |
![]() |
ed6c62217a | 1 year ago |
|
fd9b5e2063 | 1 year ago |
|
2a5de88380 | 1 year ago |
|
1043207e2d | 1 year ago |
|
5359bb12de | 1 year ago |
|
5963290f67 | 1 year ago |
|
b6f2a97d8f | 1 year ago |
|
8b407b67eb | 1 year ago |
![]() |
c3cf23da0c | 1 year ago |
|
264b46cd02 | 1 year ago |
![]() |
9835dbdcbf | 1 year ago |
![]() |
a52c20920f | 1 year ago |
![]() |
8d99066cf4 | 1 year ago |
![]() |
0a5051ceaa | 1 year ago |
![]() |
51abcd6d5d | 1 year ago |
![]() |
45b5782678 | 1 year ago |
![]() |
784beab810 | 1 year ago |
![]() |
48558e66de | 1 year ago |
![]() |
7a614325f0 | 1 year ago |
![]() |
0312cb3645 | 1 year ago |
![]() |
de698a58f5 | 1 year ago |
![]() |
628b9348b3 | 1 year ago |
![]() |
5efeb52311 | 1 year ago |
![]() |
6e5cdeca96 | 1 year ago |
![]() |
34c18e1b77 | 1 year ago |
![]() |
3c2ce3415e | 1 year ago |
![]() |
73fe9447de | 1 year ago |
![]() |
6931bfa8a5 | 1 year ago |
![]() |
0d0f6138a4 | 1 year ago |
|
00da8d7f54 | 1 year ago |
![]() |
1b435d4469 | 1 year ago |
![]() |
9a6103e78a | 1 year ago |
![]() |
15c75ee269 | 1 year ago |
![]() |
e97821a4fa | 1 year ago |
![]() |
85cd8b0383 | 1 year ago |
|
852f163e4a | 1 year ago |
|
8d444c38b4 | 1 year ago |
@ -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,25 @@
|
|||||||
|
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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -1,21 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: [
|
|
||||||
'@typescript-eslint',
|
|
||||||
'react',
|
|
||||||
'react-hooks'
|
|
||||||
],
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'plugin:react/jsx-runtime',
|
|
||||||
'plugin:react-hooks/recommended'
|
|
||||||
],
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: 'detect'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,46 +1,28 @@
|
|||||||
.vs
|
# Logs
|
||||||
.vscode
|
logs
|
||||||
.idea
|
*.log
|
||||||
.code
|
npm-debug.log*
|
||||||
.vite
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
vendor
|
node_modules
|
||||||
.nfs*
|
|
||||||
composer.lock
|
|
||||||
*.phar
|
|
||||||
dist
|
dist
|
||||||
.guard
|
dist-ssr
|
||||||
outputs
|
*.local
|
||||||
|
|
||||||
# sqlite database files
|
# Editor directories and files
|
||||||
*.sqlite
|
.vscode/*
|
||||||
|
.idea
|
||||||
views-mappings.php
|
.DS_Store
|
||||||
.env.PROD
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
.php-cs-fixer.cache
|
stats.html
|
@ -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
|
@ -1,3 +1,3 @@
|
|||||||
* [Description.md](Description.md)
|
- [Description.md](Description.md)
|
||||||
* [Conception.md](Conception.md)
|
- [Conception.md](Conception.md)
|
||||||
* [how-to-dev.md](how-to-dev.md)
|
- [how-to-dev.md](how-to-dev.md)
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -xeu
|
||||||
|
|
||||||
export OUTPUT=$1
|
export OUTPUT=$1
|
||||||
export BASE=$2
|
export BASE=$2
|
||||||
|
|
||||||
rm -rf $OUTPUT/*
|
rm -rf "$OUTPUT"/*
|
||||||
|
|
||||||
echo "VITE_API_ENDPOINT=$BASE/api" >> .env.PROD
|
echo "VITE_API_ENDPOINT=$BASE/api" >> .env.PROD
|
||||||
echo "VITE_BASE=$BASE" >> .env.PROD
|
echo "VITE_BASE=$BASE" >> .env.PROD
|
||||||
|
|
||||||
ci/build_react.msh
|
ci/build_react.msh
|
||||||
|
|
||||||
mkdir -p $OUTPUT/profiles/
|
mkdir -p "$OUTPUT"/profiles/
|
||||||
|
|
||||||
sed -E 's/\/\*PROFILE_FILE\*\/\s*".*"/"profiles\/prod-config-profile.php"/' config.php > $OUTPUT/config.php
|
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
|
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
|
cp -r vendor sql src public "$OUTPUT"
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
set -e
|
set -eu
|
||||||
|
|
||||||
mkdir ~/.ssh
|
mkdir ~/.ssh
|
||||||
echo "$SERVER_PRIVATE_KEY" > ~/.ssh/id_rsa
|
echo "$SERVER_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||||
chmod 0600 ~/.ssh
|
chmod 0600 ~/.ssh
|
||||||
chmod 0500 ~/.ssh/id_rsa*
|
chmod 0500 ~/.ssh/id_rsa*
|
||||||
|
|
||||||
SERVER_ROOT=/srv/www/IQBall
|
SERVER_ROOT=/srv/www/iqball
|
||||||
|
|
||||||
ssh -p 80 -o 'StrictHostKeyChecking=no' iqball@maxou.dev mkdir -p $SERVER_ROOT/$DRONE_BRANCH
|
ssh -p 80 -o 'StrictHostKeyChecking=no' iqball@maxou.dev mkdir -p $SERVER_ROOT/$DRONE_BRANCH
|
||||||
rsync -avz -e "ssh -p 80 -o 'StrictHostKeyChecking=no'" --delete /outputs/* iqball@maxou.dev:$SERVER_ROOT/$DRONE_BRANCH
|
rsync -avz -e "ssh -p 80 -o 'StrictHostKeyChecking=no'" --delete /outputs/* iqball@maxou.dev:$SERVER_ROOT/$DRONE_BRANCH
|
||||||
|
@ -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,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// `dev-config-profile.php` by default.
|
|
||||||
// on production server the included profile is `prod-config-profile.php`.
|
|
||||||
// Please do not touch.
|
|
||||||
|
|
||||||
require /*PROFILE_FILE*/ "profiles/dev-config-profile.php";
|
|
||||||
|
|
||||||
const SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps the given relative source uri (relative to the `/front` folder) to its actual location depending on imported profile.
|
|
||||||
* @param string $assetURI relative uri path from `/front` folder
|
|
||||||
* @return string valid url that points to the given uri
|
|
||||||
|
|
||||||
*/
|
|
||||||
function asset(string $assetURI): string {
|
|
||||||
return _asset($assetURI);
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_base_path(): string {
|
|
||||||
return _get_base_path();
|
|
||||||
}
|
|
||||||
|
|
||||||
global $_data_source_name;
|
|
||||||
$data_source_name = $_data_source_name;
|
|
||||||
const DATABASE_USER = _DATABASE_USER;
|
|
||||||
const DATABASE_PASSWORD = _DATABASE_PASSWORD;
|
|
||||||
|
|
||||||
|
|
||||||
function init_database(PDO $pdo): void {
|
|
||||||
_init_database($pdo);
|
|
||||||
}
|
|
@ -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,16 +0,0 @@
|
|||||||
import { API } from "./Constants"
|
|
||||||
|
|
||||||
export function fetchAPI(
|
|
||||||
url: string,
|
|
||||||
payload: unknown,
|
|
||||||
method = "POST",
|
|
||||||
): Promise<Response> {
|
|
||||||
return fetch(`${API}/${url}`, {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
})
|
|
||||||
}
|
|
@ -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>,
|
|
||||||
)
|
|
||||||
}
|
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 507 B |
Before Width: | Height: | Size: 405 B |
@ -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,38 +0,0 @@
|
|||||||
import React, { useRef } from "react"
|
|
||||||
import Draggable from "react-draggable"
|
|
||||||
import { BallPiece } from "./BallPiece"
|
|
||||||
import { Ball } from "../../model/tactic/Ball"
|
|
||||||
|
|
||||||
export interface CourtBallProps {
|
|
||||||
onMoved: (rect: DOMRect) => void
|
|
||||||
onRemove: () => void
|
|
||||||
ball: Ball
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CourtBall({ onMoved, ball, onRemove }: CourtBallProps) {
|
|
||||||
const pieceRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const x = ball.rightRatio
|
|
||||||
const y = ball.bottomRatio
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Draggable
|
|
||||||
onStop={() => onMoved(pieceRef.current!.getBoundingClientRect())}
|
|
||||||
nodeRef={pieceRef}>
|
|
||||||
<div
|
|
||||||
className={"ball-div"}
|
|
||||||
ref={pieceRef}
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyUp={(e) => {
|
|
||||||
if (e.key == "Delete") onRemove()
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: `${x * 100}%`,
|
|
||||||
top: `${y * 100}%`,
|
|
||||||
}}>
|
|
||||||
<BallPiece />
|
|
||||||
</div>
|
|
||||||
</Draggable>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
import { ReactNode, RefObject, useRef } from "react"
|
|
||||||
import "../../style/player.css"
|
|
||||||
import Draggable from "react-draggable"
|
|
||||||
import { PlayerPiece } from "./PlayerPiece"
|
|
||||||
import { Player } from "../../model/tactic/Player"
|
|
||||||
import { NULL_POS, ratioWithinBase } from "../arrows/Pos"
|
|
||||||
|
|
||||||
export interface PlayerProps {
|
|
||||||
player: Player
|
|
||||||
onDrag: () => void
|
|
||||||
onChange: (p: Player) => void
|
|
||||||
onRemove: () => void
|
|
||||||
courtRef: RefObject<HTMLElement>
|
|
||||||
availableActions: (ro: HTMLElement) => ReactNode[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A player that is placed on the court, which can be selected, and moved in the associated bounds
|
|
||||||
* */
|
|
||||||
export default function CourtPlayer({
|
|
||||||
player,
|
|
||||||
onDrag,
|
|
||||||
onChange,
|
|
||||||
onRemove,
|
|
||||||
courtRef,
|
|
||||||
availableActions,
|
|
||||||
}: PlayerProps) {
|
|
||||||
const hasBall = player.hasBall
|
|
||||||
const x = player.rightRatio
|
|
||||||
const y = player.bottomRatio
|
|
||||||
const pieceRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Draggable
|
|
||||||
handle=".player-piece"
|
|
||||||
nodeRef={pieceRef}
|
|
||||||
onDrag={onDrag}
|
|
||||||
//The piece is positioned using top/bottom style attributes instead
|
|
||||||
position={NULL_POS}
|
|
||||||
onStop={() => {
|
|
||||||
const pieceBounds = pieceRef.current!.getBoundingClientRect()
|
|
||||||
const parentBounds = courtRef.current!.getBoundingClientRect()
|
|
||||||
|
|
||||||
const { x, y } = ratioWithinBase(pieceBounds, parentBounds)
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
id: player.id,
|
|
||||||
rightRatio: x,
|
|
||||||
bottomRatio: y,
|
|
||||||
team: player.team,
|
|
||||||
role: player.role,
|
|
||||||
hasBall: player.hasBall,
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
<div
|
|
||||||
id={player.id}
|
|
||||||
ref={pieceRef}
|
|
||||||
className="player"
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: `${x * 100}%`,
|
|
||||||
top: `${y * 100}%`,
|
|
||||||
}}>
|
|
||||||
<div
|
|
||||||
tabIndex={0}
|
|
||||||
className="player-content"
|
|
||||||
onKeyUp={(e) => {
|
|
||||||
if (e.key == "Delete") onRemove()
|
|
||||||
}}>
|
|
||||||
<div className="player-actions">
|
|
||||||
{availableActions(pieceRef.current!)}
|
|
||||||
</div>
|
|
||||||
<PlayerPiece
|
|
||||||
team={player.team}
|
|
||||||
text={player.role}
|
|
||||||
hasBall={hasBall}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Draggable>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import { Pos } from "../../components/arrows/Pos"
|
|
||||||
import { Segment } from "../../components/arrows/BendableArrow"
|
|
||||||
import { PlayerId } from "./Player"
|
|
||||||
|
|
||||||
export enum ActionKind {
|
|
||||||
SCREEN = "SCREEN",
|
|
||||||
DRIBBLE = "DRIBBLE",
|
|
||||||
MOVE = "MOVE",
|
|
||||||
SHOOT = "SHOOT",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Action = { type: ActionKind } & MovementAction
|
|
||||||
|
|
||||||
export interface MovementAction {
|
|
||||||
fromPlayerId: PlayerId
|
|
||||||
toPlayerId?: PlayerId
|
|
||||||
moveFrom: Pos
|
|
||||||
segments: Segment[]
|
|
||||||
}
|
|
@ -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,43 +0,0 @@
|
|||||||
@import url(../theme/dark.css);
|
|
||||||
@import url(personnal_space.css);
|
|
||||||
@import url(side_menu.css);
|
|
||||||
@import url(../template/header.css);
|
|
||||||
|
|
||||||
body {
|
|
||||||
/* background-color: #303030; */
|
|
||||||
}
|
|
||||||
#main {
|
|
||||||
/* margin-left : 10%;
|
|
||||||
margin-right: 10%; */
|
|
||||||
/* border : solid 1px #303030; */
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-family: var(--font-content);
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: 0px;
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--second-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data {
|
|
||||||
border: 1.5px solid var(--main-contrast-color);
|
|
||||||
background-color: var(--main-color);
|
|
||||||
border-radius: 0.75cap;
|
|
||||||
color: var(--main-contrast-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data:hover {
|
|
||||||
border-color: var(--accent-color);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.set-button {
|
|
||||||
width: 80%;
|
|
||||||
margin-left: 5%;
|
|
||||||
margin-top: 5%;
|
|
||||||
}
|
|
@ -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,30 +0,0 @@
|
|||||||
#main {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#topbar {
|
|
||||||
display: flex;
|
|
||||||
background-color: var(--main-color);
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#court-container {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--main-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#court {
|
|
||||||
max-width: 80%;
|
|
||||||
max-height: 80%;
|
|
||||||
}
|
|
@ -1,616 +0,0 @@
|
|||||||
import {
|
|
||||||
CSSProperties,
|
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react"
|
|
||||||
import "../style/editor.css"
|
|
||||||
import TitleInput from "../components/TitleInput"
|
|
||||||
import PlainCourt from "../assets/court/full_court.svg?react"
|
|
||||||
import HalfCourt from "../assets/court/half_court.svg?react"
|
|
||||||
|
|
||||||
import { BallPiece } from "../components/editor/BallPiece"
|
|
||||||
|
|
||||||
import { Rack } from "../components/Rack"
|
|
||||||
import { PlayerPiece } from "../components/editor/PlayerPiece"
|
|
||||||
import { Player } from "../model/tactic/Player"
|
|
||||||
|
|
||||||
import { Tactic, TacticContent } from "../model/tactic/Tactic"
|
|
||||||
import { fetchAPI } from "../Fetcher"
|
|
||||||
import { PlayerTeam } from "../model/tactic/Player"
|
|
||||||
|
|
||||||
import SavingState, {
|
|
||||||
SaveState,
|
|
||||||
SaveStates,
|
|
||||||
} from "../components/editor/SavingState"
|
|
||||||
|
|
||||||
import { CourtObject } from "../model/tactic/Ball"
|
|
||||||
import { CourtAction } from "./editor/CourtAction"
|
|
||||||
import { BasketCourt } from "../components/editor/BasketCourt"
|
|
||||||
import { ratioWithinBase } from "../components/arrows/Pos"
|
|
||||||
import { Action, ActionKind } from "../model/tactic/Action"
|
|
||||||
import { BASE } from "../Constants"
|
|
||||||
|
|
||||||
const ERROR_STYLE: CSSProperties = {
|
|
||||||
borderColor: "red",
|
|
||||||
}
|
|
||||||
|
|
||||||
const GUEST_MODE_CONTENT_STORAGE_KEY = "guest_mode_content"
|
|
||||||
const GUEST_MODE_TITLE_STORAGE_KEY = "guest_mode_title"
|
|
||||||
|
|
||||||
export interface EditorViewProps {
|
|
||||||
tactic: Tactic
|
|
||||||
onContentChange: (tactic: TacticContent) => Promise<SaveState>
|
|
||||||
onNameChange: (name: string) => Promise<boolean>
|
|
||||||
courtType: "PLAIN" | "HALF"
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EditorProps {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
content: string
|
|
||||||
courtType: "PLAIN" | "HALF"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* information about a player that is into a rack
|
|
||||||
*/
|
|
||||||
interface RackedPlayer {
|
|
||||||
team: PlayerTeam
|
|
||||||
key: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type RackedCourtObject = { key: "ball" }
|
|
||||||
|
|
||||||
export default function Editor({ id, name, courtType, content }: EditorProps) {
|
|
||||||
const isInGuestMode = id == -1
|
|
||||||
|
|
||||||
const storage_content = localStorage.getItem(GUEST_MODE_CONTENT_STORAGE_KEY)
|
|
||||||
const editorContent =
|
|
||||||
isInGuestMode && storage_content != null ? storage_content : content
|
|
||||||
|
|
||||||
const storage_name = localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY)
|
|
||||||
const editorName =
|
|
||||||
isInGuestMode && storage_name != null ? storage_name : name
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EditorView
|
|
||||||
tactic={{
|
|
||||||
name: editorName,
|
|
||||||
id,
|
|
||||||
content: JSON.parse(editorContent),
|
|
||||||
}}
|
|
||||||
onContentChange={async (content: TacticContent) => {
|
|
||||||
if (isInGuestMode) {
|
|
||||||
localStorage.setItem(
|
|
||||||
GUEST_MODE_CONTENT_STORAGE_KEY,
|
|
||||||
JSON.stringify(content),
|
|
||||||
)
|
|
||||||
return SaveStates.Guest
|
|
||||||
}
|
|
||||||
return fetchAPI(`tactic/${id}/save`, { content }).then((r) =>
|
|
||||||
r.ok ? SaveStates.Ok : SaveStates.Err,
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
onNameChange={async (name: string) => {
|
|
||||||
if (isInGuestMode) {
|
|
||||||
localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name)
|
|
||||||
return true //simulate that the name has been changed
|
|
||||||
}
|
|
||||||
return fetchAPI(`tactic/${id}/edit/name`, { name }).then(
|
|
||||||
(r) => r.ok,
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
courtType={courtType}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditorView({
|
|
||||||
tactic: { id, name, content: initialContent },
|
|
||||||
onContentChange,
|
|
||||||
onNameChange,
|
|
||||||
courtType,
|
|
||||||
}: EditorViewProps) {
|
|
||||||
const isInGuestMode = id == -1
|
|
||||||
|
|
||||||
const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
|
|
||||||
const [content, setContent, saveState] = useContentState(
|
|
||||||
initialContent,
|
|
||||||
isInGuestMode ? SaveStates.Guest : SaveStates.Ok,
|
|
||||||
useMemo(() => debounceAsync(onContentChange),
|
|
||||||
[onContentChange],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const [allies, setAllies] = useState(
|
|
||||||
getRackPlayers(PlayerTeam.Allies, content.players),
|
|
||||||
)
|
|
||||||
const [opponents, setOpponents] = useState(
|
|
||||||
getRackPlayers(PlayerTeam.Opponents, content.players),
|
|
||||||
)
|
|
||||||
|
|
||||||
const [objects, setObjects] = useState<RackedCourtObject[]>(
|
|
||||||
isBallOnCourt(content) ? [] : [{ key: "ball" }],
|
|
||||||
)
|
|
||||||
|
|
||||||
const courtDivContentRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const isBoundsOnCourt = (bounds: DOMRect) => {
|
|
||||||
const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
|
|
||||||
|
|
||||||
// check if refBounds overlaps courtBounds
|
|
||||||
return !(
|
|
||||||
bounds.top > courtBounds.bottom ||
|
|
||||||
bounds.right < courtBounds.left ||
|
|
||||||
bounds.bottom < courtBounds.top ||
|
|
||||||
bounds.left > courtBounds.right
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => {
|
|
||||||
const refBounds = ref.getBoundingClientRect()
|
|
||||||
const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
|
|
||||||
|
|
||||||
const { x, y } = ratioWithinBase(refBounds, courtBounds)
|
|
||||||
|
|
||||||
setContent((content) => {
|
|
||||||
return {
|
|
||||||
...content,
|
|
||||||
players: [
|
|
||||||
...content.players,
|
|
||||||
{
|
|
||||||
id: "player-" + element.key + "-" + element.team,
|
|
||||||
team: element.team,
|
|
||||||
role: element.key,
|
|
||||||
rightRatio: x,
|
|
||||||
bottomRatio: y,
|
|
||||||
hasBall: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
actions: content.actions,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onObjectDetach = (
|
|
||||||
ref: HTMLDivElement,
|
|
||||||
rackedObject: RackedCourtObject,
|
|
||||||
) => {
|
|
||||||
const refBounds = ref.getBoundingClientRect()
|
|
||||||
const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
|
|
||||||
|
|
||||||
const { x, y } = ratioWithinBase(refBounds, courtBounds)
|
|
||||||
|
|
||||||
let courtObject: CourtObject
|
|
||||||
|
|
||||||
switch (rackedObject.key) {
|
|
||||||
case "ball":
|
|
||||||
const ballObj = content.objects.findIndex(
|
|
||||||
(o) => o.type == "ball",
|
|
||||||
)
|
|
||||||
const playerCollidedIdx = getPlayerCollided(
|
|
||||||
refBounds,
|
|
||||||
content.players,
|
|
||||||
)
|
|
||||||
if (playerCollidedIdx != -1) {
|
|
||||||
onBallDropOnPlayer(playerCollidedIdx)
|
|
||||||
setContent((content) => {
|
|
||||||
return {
|
|
||||||
...content,
|
|
||||||
objects: content.objects.toSpliced(ballObj, 1),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
courtObject = {
|
|
||||||
type: "ball",
|
|
||||||
rightRatio: x,
|
|
||||||
bottomRatio: y,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error("unknown court object " + rackedObject.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
setContent((content) => {
|
|
||||||
return {
|
|
||||||
...content,
|
|
||||||
objects: [...content.objects, courtObject],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPlayerCollided = (
|
|
||||||
bounds: DOMRect,
|
|
||||||
players: Player[],
|
|
||||||
): number | -1 => {
|
|
||||||
for (let i = 0; i < players.length; i++) {
|
|
||||||
const player = players[i]
|
|
||||||
const playerBounds = document
|
|
||||||
.getElementById(player.id)!
|
|
||||||
.getBoundingClientRect()
|
|
||||||
const doesOverlap = !(
|
|
||||||
bounds.top > playerBounds.bottom ||
|
|
||||||
bounds.right < playerBounds.left ||
|
|
||||||
bounds.bottom < playerBounds.top ||
|
|
||||||
bounds.left > playerBounds.right
|
|
||||||
)
|
|
||||||
if (doesOverlap) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateActions(actions: Action[], players: Player[]) {
|
|
||||||
return actions.map((action) => {
|
|
||||||
const originHasBall = players.find(
|
|
||||||
(p) => p.id == action.fromPlayerId,
|
|
||||||
)!.hasBall
|
|
||||||
|
|
||||||
let type = action.type
|
|
||||||
|
|
||||||
if (originHasBall && type == ActionKind.MOVE) {
|
|
||||||
type = ActionKind.DRIBBLE
|
|
||||||
} else if (originHasBall && type == ActionKind.SCREEN) {
|
|
||||||
type = ActionKind.SHOOT
|
|
||||||
} else if (type == ActionKind.DRIBBLE) {
|
|
||||||
type = ActionKind.MOVE
|
|
||||||
} else if (type == ActionKind.SHOOT) {
|
|
||||||
type = ActionKind.SCREEN
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...action,
|
|
||||||
type,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onBallDropOnPlayer = (playerCollidedIdx: number) => {
|
|
||||||
setContent((content) => {
|
|
||||||
const ballObj = content.objects.findIndex((o) => o.type == "ball")
|
|
||||||
let player = content.players.at(playerCollidedIdx) as Player
|
|
||||||
const players = content.players.toSpliced(playerCollidedIdx, 1, {
|
|
||||||
...player,
|
|
||||||
hasBall: true,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
...content,
|
|
||||||
actions: updateActions(content.actions, players),
|
|
||||||
players,
|
|
||||||
objects: content.objects.toSpliced(ballObj, 1),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onBallDrop = (refBounds: DOMRect) => {
|
|
||||||
if (!isBoundsOnCourt(refBounds)) {
|
|
||||||
removeCourtBall()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const playerCollidedIdx = getPlayerCollided(refBounds, content.players)
|
|
||||||
if (playerCollidedIdx != -1) {
|
|
||||||
setContent((content) => {
|
|
||||||
return {
|
|
||||||
...content,
|
|
||||||
players: content.players.map((player) => ({
|
|
||||||
...player,
|
|
||||||
hasBall: false,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onBallDropOnPlayer(playerCollidedIdx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.objects.findIndex((o) => o.type == "ball") != -1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
|
|
||||||
const { x, y } = ratioWithinBase(refBounds, courtBounds)
|
|
||||||
let courtObject: CourtObject
|
|
||||||
|
|
||||||
courtObject = {
|
|
||||||
type: "ball",
|
|
||||||
rightRatio: x,
|
|
||||||
bottomRatio: y,
|
|
||||||
}
|
|
||||||
|
|
||||||
const players = content.players.map((player) => ({
|
|
||||||
...player,
|
|
||||||
hasBall: false,
|
|
||||||
}))
|
|
||||||
|
|
||||||
setContent((content) => {
|
|
||||||
return {
|
|
||||||
...content,
|
|
||||||
actions: updateActions(content.actions, players),
|
|
||||||
players,
|
|
||||||
objects: [...content.objects, courtObject],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const removePlayer = (player: Player) => {
|
|
||||||
setContent((content) => ({
|
|
||||||
...content,
|
|
||||||
players: toSplicedPlayers(content.players, player, false),
|
|
||||||
objects: [...content.objects],
|
|
||||||
actions: content.actions.filter(
|
|
||||||
(a) =>
|
|
||||||
a.toPlayerId !== player.id && a.fromPlayerId !== player.id,
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
let setter
|
|
||||||
switch (player.team) {
|
|
||||||
case PlayerTeam.Opponents:
|
|
||||||
setter = setOpponents
|
|
||||||
break
|
|
||||||
case PlayerTeam.Allies:
|
|
||||||
setter = setAllies
|
|
||||||
}
|
|
||||||
if (player.hasBall) {
|
|
||||||
setObjects([{ key: "ball" }])
|
|
||||||
}
|
|
||||||
setter((players) => [
|
|
||||||
...players,
|
|
||||||
{
|
|
||||||
team: player.team,
|
|
||||||
pos: player.role,
|
|
||||||
key: player.role,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeCourtBall = () => {
|
|
||||||
setContent((content) => {
|
|
||||||
const ballObj = content.objects.findIndex((o) => o.type == "ball")
|
|
||||||
return {
|
|
||||||
...content,
|
|
||||||
players: content.players.map((player) => ({
|
|
||||||
...player,
|
|
||||||
hasBall: false,
|
|
||||||
})),
|
|
||||||
objects: content.objects.toSpliced(ballObj, 1),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setObjects([{ key: "ball" }])
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="main-div">
|
|
||||||
<div id="topbar-div">
|
|
||||||
<button onClick={() => (location.pathname = BASE + "/")}>
|
|
||||||
Home
|
|
||||||
</button>
|
|
||||||
<div id="topbar-left">
|
|
||||||
<SavingState state={saveState} />
|
|
||||||
</div>
|
|
||||||
<div id="title-input-div">
|
|
||||||
<TitleInput
|
|
||||||
style={titleStyle}
|
|
||||||
default_value={name}
|
|
||||||
on_validated={(new_name) => {
|
|
||||||
onNameChange(new_name).then((success) => {
|
|
||||||
setTitleStyle(success ? {} : ERROR_STYLE)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div id="topbar-right" />
|
|
||||||
</div>
|
|
||||||
<div id="edit-div">
|
|
||||||
<div id="racks">
|
|
||||||
<Rack
|
|
||||||
id="allies-rack"
|
|
||||||
objects={allies}
|
|
||||||
onChange={setAllies}
|
|
||||||
canDetach={(div) =>
|
|
||||||
isBoundsOnCourt(div.getBoundingClientRect())
|
|
||||||
}
|
|
||||||
onElementDetached={onPieceDetach}
|
|
||||||
render={({ team, key }) => (
|
|
||||||
<PlayerPiece
|
|
||||||
team={team}
|
|
||||||
text={key}
|
|
||||||
key={key}
|
|
||||||
hasBall={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Rack
|
|
||||||
id={"objects"}
|
|
||||||
objects={objects}
|
|
||||||
onChange={setObjects}
|
|
||||||
canDetach={(div) =>
|
|
||||||
isBoundsOnCourt(div.getBoundingClientRect())
|
|
||||||
}
|
|
||||||
onElementDetached={onObjectDetach}
|
|
||||||
render={renderCourtObject}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Rack
|
|
||||||
id="opponent-rack"
|
|
||||||
objects={opponents}
|
|
||||||
onChange={setOpponents}
|
|
||||||
canDetach={(div) =>
|
|
||||||
isBoundsOnCourt(div.getBoundingClientRect())
|
|
||||||
}
|
|
||||||
onElementDetached={onPieceDetach}
|
|
||||||
render={({ team, key }) => (
|
|
||||||
<PlayerPiece
|
|
||||||
team={team}
|
|
||||||
text={key}
|
|
||||||
key={key}
|
|
||||||
hasBall={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div id="court-div">
|
|
||||||
<div id="court-div-bounds">
|
|
||||||
<BasketCourt
|
|
||||||
players={content.players}
|
|
||||||
objects={content.objects}
|
|
||||||
actions={content.actions}
|
|
||||||
onBallMoved={onBallDrop}
|
|
||||||
courtImage={<Court courtType={courtType} />}
|
|
||||||
courtRef={courtDivContentRef}
|
|
||||||
setActions={(actions) =>
|
|
||||||
setContent((content) => ({
|
|
||||||
...content,
|
|
||||||
players: content.players,
|
|
||||||
actions: actions(content.actions),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
renderAction={(action, i) => (
|
|
||||||
<CourtAction
|
|
||||||
key={i}
|
|
||||||
action={action}
|
|
||||||
courtRef={courtDivContentRef}
|
|
||||||
onActionDeleted={() => {
|
|
||||||
setContent((content) => ({
|
|
||||||
...content,
|
|
||||||
actions: content.actions.toSpliced(
|
|
||||||
i,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
onActionChanges={(a) =>
|
|
||||||
setContent((content) => ({
|
|
||||||
...content,
|
|
||||||
actions: content.actions.toSpliced(
|
|
||||||
i,
|
|
||||||
1,
|
|
||||||
a,
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
onPlayerChange={(player) => {
|
|
||||||
const playerBounds = document
|
|
||||||
.getElementById(player.id)!
|
|
||||||
.getBoundingClientRect()
|
|
||||||
if (!isBoundsOnCourt(playerBounds)) {
|
|
||||||
removePlayer(player)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setContent((content) => ({
|
|
||||||
...content,
|
|
||||||
players: toSplicedPlayers(
|
|
||||||
content.players,
|
|
||||||
player,
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
onPlayerRemove={removePlayer}
|
|
||||||
onBallRemove={removeCourtBall}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBallOnCourt(content: TacticContent) {
|
|
||||||
if (content.players.findIndex((p) => p.hasBall) != -1) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return content.objects.findIndex((o) => o.type == "ball") != -1
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCourtObject(courtObject: RackedCourtObject) {
|
|
||||||
if (courtObject.key == "ball") {
|
|
||||||
return <BallPiece />
|
|
||||||
}
|
|
||||||
throw new Error("unknown racked court object " + courtObject.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Court({ courtType }: { courtType: string }) {
|
|
||||||
return (
|
|
||||||
<div id="court-image-div">
|
|
||||||
{courtType == "PLAIN" ? (
|
|
||||||
<PlainCourt id="court-image" />
|
|
||||||
) : (
|
|
||||||
<HalfCourt id="court-image" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRackPlayers(team: PlayerTeam, players: Player[]): RackedPlayer[] {
|
|
||||||
return ["1", "2", "3", "4", "5"]
|
|
||||||
.filter(
|
|
||||||
(role) =>
|
|
||||||
players.findIndex((p) => p.team == team && p.role == role) ==
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
.map((key) => ({ team, key }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function debounceAsync<A, B>(
|
|
||||||
f: (args: A) => Promise<B>,
|
|
||||||
delay = 1000,
|
|
||||||
): (args: A) => Promise<B> {
|
|
||||||
let task = 0
|
|
||||||
return (args: A) => {
|
|
||||||
clearTimeout(task)
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
task = setTimeout(() => f(args).then(resolve).catch(reject), delay)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function useContentState<S>(
|
|
||||||
initialContent: S,
|
|
||||||
initialSaveState: SaveState,
|
|
||||||
saveStateCallback: (s: S) => Promise<SaveState>,
|
|
||||||
): [S, Dispatch<SetStateAction<S>>, SaveState] {
|
|
||||||
const [content, setContent] = useState(initialContent)
|
|
||||||
const [savingState, setSavingState] = useState(initialSaveState)
|
|
||||||
|
|
||||||
const setContentSynced = useCallback(
|
|
||||||
(newState: SetStateAction<S>) => {
|
|
||||||
setContent((content) => {
|
|
||||||
const state =
|
|
||||||
typeof newState === "function"
|
|
||||||
? (newState as (state: S) => S)(content)
|
|
||||||
: newState
|
|
||||||
if (state !== content) {
|
|
||||||
setSavingState(SaveStates.Saving)
|
|
||||||
saveStateCallback(state)
|
|
||||||
.then(setSavingState)
|
|
||||||
.catch(() => setSavingState(SaveStates.Err))
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[saveStateCallback],
|
|
||||||
)
|
|
||||||
|
|
||||||
return [content, setContentSynced, savingState]
|
|
||||||
}
|
|
||||||
|
|
||||||
function toSplicedPlayers(
|
|
||||||
players: Player[],
|
|
||||||
player: Player,
|
|
||||||
replace: boolean,
|
|
||||||
): Player[] {
|
|
||||||
const idx = players.findIndex(
|
|
||||||
(p) => p.team === player.team && p.role === player.role,
|
|
||||||
)
|
|
||||||
|
|
||||||
return players.toSpliced(idx, 1, ...(replace ? [player] : []))
|
|
||||||
}
|
|
@ -1,259 +0,0 @@
|
|||||||
import "../style/home/home.css"
|
|
||||||
|
|
||||||
// import AccountSvg from "../assets/account.svg?react"
|
|
||||||
import { Header } from "./template/Header"
|
|
||||||
import { BASE } from "../Constants"
|
|
||||||
|
|
||||||
interface Tactic {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
creation_date: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Team {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
picture: string
|
|
||||||
main_color: string
|
|
||||||
second_color: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home({
|
|
||||||
lastTactics,
|
|
||||||
allTactics,
|
|
||||||
teams,
|
|
||||||
username,
|
|
||||||
}: {
|
|
||||||
lastTactics: Tactic[]
|
|
||||||
allTactics: Tactic[]
|
|
||||||
teams: Team[]
|
|
||||||
username: string
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div id="main">
|
|
||||||
<Header username={username} />
|
|
||||||
<Body
|
|
||||||
lastTactics={lastTactics}
|
|
||||||
allTactics={allTactics}
|
|
||||||
teams={teams}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Body({
|
|
||||||
lastTactics,
|
|
||||||
allTactics,
|
|
||||||
teams,
|
|
||||||
}: {
|
|
||||||
lastTactics: Tactic[]
|
|
||||||
allTactics: Tactic[]
|
|
||||||
teams: Team[]
|
|
||||||
}) {
|
|
||||||
const widthPersonalSpace = 78
|
|
||||||
const widthSideMenu = 100 - widthPersonalSpace
|
|
||||||
return (
|
|
||||||
<div id="body">
|
|
||||||
<PersonalSpace width={widthPersonalSpace} allTactics={allTactics} />
|
|
||||||
<SideMenu
|
|
||||||
width={widthSideMenu}
|
|
||||||
lastTactics={lastTactics}
|
|
||||||
teams={teams}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SideMenu({
|
|
||||||
width,
|
|
||||||
lastTactics,
|
|
||||||
teams,
|
|
||||||
}: {
|
|
||||||
width: number
|
|
||||||
lastTactics: Tactic[]
|
|
||||||
teams: Team[]
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="side-menu"
|
|
||||||
style={{
|
|
||||||
width: width + "%",
|
|
||||||
}}>
|
|
||||||
<div id="side-menu-content">
|
|
||||||
<Team teams={teams} />
|
|
||||||
<Tactic lastTactics={lastTactics} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PersonalSpace({
|
|
||||||
width,
|
|
||||||
allTactics,
|
|
||||||
}: {
|
|
||||||
width: number
|
|
||||||
allTactics: Tactic[]
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="personal-space"
|
|
||||||
style={{
|
|
||||||
width: width + "%",
|
|
||||||
}}>
|
|
||||||
<TitlePersonalSpace />
|
|
||||||
<BodyPersonalSpace allTactics={allTactics} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TitlePersonalSpace() {
|
|
||||||
return (
|
|
||||||
<div id="title-personal-space">
|
|
||||||
<h2>Espace Personnel</h2>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableData({ allTactics }: { allTactics: Tactic[] }) {
|
|
||||||
const nbRow = Math.floor(allTactics.length / 3) + 1
|
|
||||||
let listTactic = Array(nbRow)
|
|
||||||
for (let i = 0; i < nbRow; i++) {
|
|
||||||
listTactic[i] = Array(0)
|
|
||||||
}
|
|
||||||
let i = 0
|
|
||||||
let j = 0
|
|
||||||
allTactics.forEach((tactic) => {
|
|
||||||
listTactic[i].push(tactic)
|
|
||||||
j++
|
|
||||||
if (j === 3) {
|
|
||||||
i++
|
|
||||||
j = 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while (i < nbRow) {
|
|
||||||
listTactic[i] = listTactic[i].map((tactic: Tactic) => (
|
|
||||||
<td
|
|
||||||
key={tactic.id}
|
|
||||||
className="data"
|
|
||||||
onClick={() => {
|
|
||||||
location.pathname = BASE + "/tactic/" + tactic.id + "/edit"
|
|
||||||
}}>
|
|
||||||
{truncateString(tactic.name, 25)}
|
|
||||||
</td>
|
|
||||||
))
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if (nbRow == 1) {
|
|
||||||
if (listTactic[0].length < 3) {
|
|
||||||
for (let i = 0; i <= 3 - listTactic[0].length; i++) {
|
|
||||||
listTactic[0].push(<td key={"tdNone" + i}></td>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = listTactic.map((tactic, rowIndex) => (
|
|
||||||
<tr key={rowIndex + "row"}>{tactic}</tr>
|
|
||||||
))
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) {
|
|
||||||
let data
|
|
||||||
if (allTactics.length == 0) {
|
|
||||||
data = <p>Aucune tactique créée !</p>
|
|
||||||
} else {
|
|
||||||
data = <TableData allTactics={allTactics} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="body-personal-space">
|
|
||||||
<table>
|
|
||||||
<tbody key="tbody">{data}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Team({ teams }: { teams: Team[] }) {
|
|
||||||
return (
|
|
||||||
<div id="teams">
|
|
||||||
<div className="titre-side-menu">
|
|
||||||
<h2 className="title">Mes équipes</h2>
|
|
||||||
<button
|
|
||||||
className="new"
|
|
||||||
onClick={() => (location.pathname = BASE + "/team/new")}>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<SetButtonTeam teams={teams} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Tactic({ lastTactics }: { lastTactics: Tactic[] }) {
|
|
||||||
return (
|
|
||||||
<div id="tactic">
|
|
||||||
<div className="titre-side-menu">
|
|
||||||
<h2 className="title">Mes dernières stratégies</h2>
|
|
||||||
<button
|
|
||||||
className="new"
|
|
||||||
id="create-tactic"
|
|
||||||
onClick={() => (location.pathname = BASE + "/tactic/new")}>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<SetButtonTactic tactics={lastTactics} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SetButtonTactic({ tactics }: { tactics: Tactic[] }) {
|
|
||||||
const lastTactics = tactics.map((tactic) => (
|
|
||||||
<ButtonLastTactic tactic={tactic} />
|
|
||||||
))
|
|
||||||
return <div className="set-button">{lastTactics}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function SetButtonTeam({ teams }: { teams: Team[] }) {
|
|
||||||
const listTeam = teams.map((teams) => <ButtonTeam team={teams} />)
|
|
||||||
return <div className="set-button">{listTeam}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function ButtonTeam({ team }: { team: Team }) {
|
|
||||||
const name = truncateString(team.name, 20)
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
id={"button-team" + team.id}
|
|
||||||
className="button-side-menu data"
|
|
||||||
onClick={() => {
|
|
||||||
location.pathname = BASE + "/team/" + team.id
|
|
||||||
}}>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ButtonLastTactic({ tactic }: { tactic: Tactic }) {
|
|
||||||
const name = truncateString(tactic.name, 20)
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id={"button" + tactic.id}
|
|
||||||
className="button-side-menu data"
|
|
||||||
onClick={() => {
|
|
||||||
location.pathname = BASE + "/tactic/" + tactic.id + "/edit"
|
|
||||||
}}>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function truncateString(name: string, limit: number): string {
|
|
||||||
if (name.length > limit) {
|
|
||||||
name = name.substring(0, limit) + "..."
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
import { BASE } from "../../Constants"
|
|
||||||
import accountSvg from "../../assets/account.svg"
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param param0 username
|
|
||||||
* @returns Header
|
|
||||||
*/
|
|
||||||
export function Header({ username }: { username: string }) {
|
|
||||||
return (
|
|
||||||
<div id="header">
|
|
||||||
<div id="header-left"></div>
|
|
||||||
<div id="header-center">
|
|
||||||
<h1
|
|
||||||
id="iqball"
|
|
||||||
className="clickable"
|
|
||||||
onClick={() => {
|
|
||||||
location.pathname = BASE + "/"
|
|
||||||
}}>
|
|
||||||
<span id="IQ">IQ</span>
|
|
||||||
<span id="Ball">Ball</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div id="header-right">
|
|
||||||
<div className="clickable" id="clickable-header-right">
|
|
||||||
{/* <AccountSvg id="img-account" /> */}
|
|
||||||
<img
|
|
||||||
id="img-account"
|
|
||||||
src={accountSvg}
|
|
||||||
onClick={() => {
|
|
||||||
location.pathname = BASE + "/settings"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p id="username">{username}</p>
|
|
||||||
</div>
|
|
||||||
</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,38 +1,43 @@
|
|||||||
{
|
{
|
||||||
"name": "iqball_web",
|
"name": "iqball_web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"type": "module",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"dependencies": {
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/node": "^16.18.59",
|
"@types/react": "^18.2.31",
|
||||||
"@types/react": "^18.2.31",
|
"@types/react-dom": "^18.2.14",
|
||||||
"@types/react-dom": "^18.2.14",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-draggable": "^4.4.6",
|
"react-drag-drop-files": "^2.3.10",
|
||||||
"typescript": "^5.2.2",
|
"react-draggable": "^4.4.6",
|
||||||
"vite": "^4.5.0",
|
"react-router-dom": "^6.22.0",
|
||||||
"vite-plugin-css-injected-by-js": "^3.3.0"
|
"typescript": "^5.2.2",
|
||||||
},
|
"vite": "^4.5.0",
|
||||||
"scripts": {
|
"vite-plugin-css-injected-by-js": "^3.3.0"
|
||||||
"start": "vite --host",
|
},
|
||||||
"build": "vite build",
|
"scripts": {
|
||||||
"test": "vite test",
|
"start": "vite --host",
|
||||||
"format": "prettier --config .prettierrc 'front' --write",
|
"build": "vite build",
|
||||||
"tsc": "tsc"
|
"test": "vitest",
|
||||||
},
|
"format": "prettier --config .prettierrc '.' --write",
|
||||||
"devDependencies": {
|
"tsc": "tsc"
|
||||||
"@vitejs/plugin-react": "^4.1.0",
|
},
|
||||||
"prettier": "^3.1.0",
|
"devDependencies": {
|
||||||
"typescript": "^5.2.2",
|
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||||
"vite-plugin-svgr": "^4.1.0",
|
"@typescript-eslint/parser": "^6.11.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
"@vitejs/plugin-react": "^4.1.0",
|
||||||
"@typescript-eslint/parser": "^6.11.0",
|
"eslint": "^8.53.0",
|
||||||
"eslint": "^8.53.0",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0"
|
"jsdom": "^24.0.0",
|
||||||
}
|
"prettier": "^3.1.0",
|
||||||
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite-plugin-svgr": "^4.1.0",
|
||||||
|
"vitest": "^1.3.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use IQBall\Core\Connection;
|
|
||||||
use IQBall\Core\Gateway\AccountGateway;
|
|
||||||
use IQBall\Core\Model\AuthModel;
|
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _init_database(PDO $pdo): void {
|
|
||||||
$accounts = new AccountGateway(new Connection($pdo));
|
|
||||||
$teams = new \IQBall\Core\Gateway\TeamGateway((new Connection($pdo)));
|
|
||||||
|
|
||||||
$defaultAccounts = ["maxime", "mael", "yanis", "vivien"];
|
|
||||||
$defaultTeams = ["Lakers", "Celtics", "Bulls"];
|
|
||||||
|
|
||||||
|
|
||||||
foreach ($defaultAccounts as $name) {
|
|
||||||
$email = "$name@mail.com";
|
|
||||||
$id = $accounts->insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT), "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png");
|
|
||||||
$accounts->setIsAdmin($id, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($defaultTeams as $name) {
|
|
||||||
$id = $teams->insert($name, "https://lebasketographe.fr/wp-content/uploads/2019/11/nom-equipes-nba.jpg", "#1a2b3c", "#FF00AA");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _get_base_path(): string {
|
|
||||||
return "";
|
|
||||||
}
|
|
@ -1,32 +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";
|
|
||||||
|
|
||||||
// THIS VALUE IS TO SET IN THE CI
|
|
||||||
const BASE_PATH = null;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function _init_database(PDO $pdo): void {}
|
|
||||||
|
|
||||||
function _get_base_path(): string {
|
|
||||||
return BASE_PATH;
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
RewriteEngine on
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^.*$ ./index.php [NC,L,QSA]
|
|
@ -1,4 +0,0 @@
|
|||||||
RewriteEngine on
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^.*$ ./index.php [NC,L,QSA]
|
|
@ -1,96 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require "../../config.php";
|
|
||||||
require "../../vendor/autoload.php";
|
|
||||||
require "../../sql/database.php";
|
|
||||||
|
|
||||||
use IQBall\Api\API;
|
|
||||||
use IQBall\Api\Controller\APIAccountsController;
|
|
||||||
use IQBall\Api\Controller\APIAuthController;
|
|
||||||
use IQBall\Api\Controller\APIServerController;
|
|
||||||
use IQBall\Api\Controller\APITacticController;
|
|
||||||
use IQBall\App\Session\PhpSessionHandle;
|
|
||||||
use IQBall\Core\Action;
|
|
||||||
use IQBall\Core\Connection;
|
|
||||||
use IQBall\Core\Data\Account;
|
|
||||||
use IQBall\Core\Gateway\AccountGateway;
|
|
||||||
use IQBall\Core\Gateway\TacticInfoGateway;
|
|
||||||
use IQBall\Core\Model\AuthModel;
|
|
||||||
use IQBall\Core\Model\TacticModel;
|
|
||||||
|
|
||||||
|
|
||||||
$basePath = get_base_path() . "/api";
|
|
||||||
|
|
||||||
function getTacticController(): APITacticController {
|
|
||||||
return new APITacticController(new TacticModel(new TacticInfoGateway(new Connection(get_database()))));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuthController(): APIAuthController {
|
|
||||||
return new APIAuthController(new AuthModel(new AccountGateway(new Connection(get_database()))));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAccountController(): APIAccountsController {
|
|
||||||
$con = new Connection(get_database());
|
|
||||||
$gw = new AccountGateway($con);
|
|
||||||
return new APIAccountsController(new AuthModel($gw), $gw);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function getServerController(): APIServerController {
|
|
||||||
global $basePath;
|
|
||||||
return new APIServerController($basePath, get_database());
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAPITeamController(): \IQBall\Api\Controller\APITeamController {
|
|
||||||
$con = new Connection(get_database());
|
|
||||||
return new \IQBall\Api\Controller\APITeamController(new \IQBall\Core\Model\TeamModel(new \IQBall\Core\Gateway\TeamGateway($con), new \IQBall\Core\Gateway\MemberGateway($con), new AccountGateway($con)));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function getRoutes(): AltoRouter {
|
|
||||||
global $basePath;
|
|
||||||
$router = new AltoRouter();
|
|
||||||
$router->setBasePath($basePath);
|
|
||||||
|
|
||||||
$router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize()));
|
|
||||||
$router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc)));
|
|
||||||
$router->map("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id, Account $acc) => getTacticController()->saveContent($id, $acc)));
|
|
||||||
$router->map("GET", "/admin/list-users", Action::noAuth(fn() => getAccountController()->listUsers($_GET)));
|
|
||||||
$router->map("GET", "/admin/user/[i:id]", Action::noAuth(fn(int $id) => getAccountController()->getUser($id)));
|
|
||||||
$router->map("GET", "/admin/user/[i:id]/space", Action::noAuth(fn(int $id) => getTacticController()->getUserTactics($id)));
|
|
||||||
$router->map("POST", "/admin/user/add", Action::noAuth(fn() => getAccountController()->addUser()));
|
|
||||||
$router->map("POST", "/admin/user/remove-all", Action::noAuth(fn() => getAccountController()->removeUsers()));
|
|
||||||
$router->map("POST", "/admin/user/[i:id]/update", Action::noAuth(fn(int $id) => getAccountController()->updateUser($id)));
|
|
||||||
$router->map("GET", "/admin/server-info", Action::noAuth(fn() => getServerController()->getServerInfo()));
|
|
||||||
$router->map("GET", "/admin/list-team", Action::noAuth(fn() => getAPITeamController()->listTeams($_GET)));
|
|
||||||
$router->map("POST", "/admin/add-team", Action::noAuth(fn() => getAPITeamController()->addTeam()));
|
|
||||||
$router->map("POST", "/admin/delete-teams", Action::noAuth(fn() => getAPITeamController()->deleteTeamSelected()));
|
|
||||||
$router->map("POST", "/admin/team/[i:id]/update", Action::noAuth(fn(int $id) => getAPITeamController()->updateTeam($id)));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return $router;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the way of being authorised through the API
|
|
||||||
* By checking if an Authorisation header is set, and by expecting its value to be a valid token of an account.
|
|
||||||
* If the header is not set, fallback to the App's PHP session system, and try to extract the account from it.
|
|
||||||
* @return Account|null
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
function tryGetAuthorization(): ?Account {
|
|
||||||
$headers = getallheaders();
|
|
||||||
|
|
||||||
// If no authorization header is set, try fallback to php session.
|
|
||||||
if (!isset($headers['Authorization'])) {
|
|
||||||
$session = PhpSessionHandle::init();
|
|
||||||
return $session->getAccount();
|
|
||||||
}
|
|
||||||
$token = $headers['Authorization'];
|
|
||||||
$gateway = new AccountGateway(new Connection(get_database()));
|
|
||||||
return $gateway->getAccountFromToken($token);
|
|
||||||
}
|
|
||||||
|
|
||||||
Api::consume(API::handleMatch(getRoutes()->match(), fn() => tryGetAuthorization()));
|
|
@ -1 +0,0 @@
|
|||||||
../front/assets
|
|
@ -1 +0,0 @@
|
|||||||
../front
|
|
@ -1,129 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require "../vendor/autoload.php";
|
|
||||||
require "../config.php";
|
|
||||||
require "../sql/database.php";
|
|
||||||
require "../src/App/react-display.php";
|
|
||||||
|
|
||||||
use IQBall\App\App;
|
|
||||||
use IQBall\App\Controller\AuthController;
|
|
||||||
use IQBall\App\Controller\EditorController;
|
|
||||||
use IQBall\App\Controller\TeamController;
|
|
||||||
use IQBall\App\Controller\UserController;
|
|
||||||
use IQBall\App\Controller\VisualizerController;
|
|
||||||
use IQBall\App\Session\MutableSessionHandle;
|
|
||||||
use IQBall\App\Session\PhpSessionHandle;
|
|
||||||
use IQBall\App\Session\SessionHandle;
|
|
||||||
use IQBall\App\ViewHttpResponse;
|
|
||||||
use IQBall\Core\Action;
|
|
||||||
use IQBall\Core\Connection;
|
|
||||||
use IQBall\Core\Data\CourtType;
|
|
||||||
use IQBall\Core\Gateway\AccountGateway;
|
|
||||||
use IQBall\Core\Gateway\MemberGateway;
|
|
||||||
use IQBall\Core\Gateway\TacticInfoGateway;
|
|
||||||
use IQBall\Core\Gateway\TeamGateway;
|
|
||||||
use IQBall\Core\Http\HttpCodes;
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
use IQBall\Core\Model\AuthModel;
|
|
||||||
use IQBall\Core\Model\TacticModel;
|
|
||||||
use IQBall\Core\Model\TeamModel;
|
|
||||||
use IQBall\Core\Validation\ValidationFail;
|
|
||||||
use Twig\Environment;
|
|
||||||
use Twig\Loader\FilesystemLoader;
|
|
||||||
use Twig\TwigFunction;
|
|
||||||
|
|
||||||
function getConnection(): Connection {
|
|
||||||
return new Connection(get_database());
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserController(): UserController {
|
|
||||||
return new UserController(new TacticModel(new TacticInfoGateway(getConnection())), new TeamModel(new TeamGateway(getConnection()), new MemberGateway(getConnection()), new AccountGateway(getConnection())));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVisualizerController(): VisualizerController {
|
|
||||||
return new VisualizerController(new TacticModel(new TacticInfoGateway(getConnection())));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEditorController(): EditorController {
|
|
||||||
return new EditorController(new TacticModel(new TacticInfoGateway(getConnection())));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTeamController(): TeamController {
|
|
||||||
$con = getConnection();
|
|
||||||
return new TeamController(new TeamModel(new TeamGateway($con), new MemberGateway($con), new AccountGateway($con)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuthController(): AuthController {
|
|
||||||
return new AuthController(new AuthModel(new AccountGateway(getConnection())));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTwig(): Environment {
|
|
||||||
global $basePath;
|
|
||||||
$fl = new FilesystemLoader("../src/App/Views");
|
|
||||||
$twig = new Environment($fl);
|
|
||||||
|
|
||||||
$twig->addFunction(new TwigFunction('path', fn(string $str) => "$basePath$str"));
|
|
||||||
|
|
||||||
return $twig;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRoutes(): AltoRouter {
|
|
||||||
global $basePath;
|
|
||||||
|
|
||||||
$ar = new AltoRouter();
|
|
||||||
$ar->setBasePath($basePath);
|
|
||||||
|
|
||||||
//authentication
|
|
||||||
$ar->map("GET", "/login", Action::noAuth(fn() => getAuthController()->displayLogin()));
|
|
||||||
$ar->map("GET", "/register", Action::noAuth(fn() => getAuthController()->displayRegister()));
|
|
||||||
$ar->map("POST", "/login", Action::noAuth(fn(SessionHandle $s) => getAuthController()->login($_POST, $s)));
|
|
||||||
$ar->map("POST", "/register", Action::noAuth(fn(SessionHandle $s) => getAuthController()->register($_POST, $s)));
|
|
||||||
|
|
||||||
//user-related
|
|
||||||
$ar->map("GET", "/", Action::auth(fn(SessionHandle $s) => getUserController()->home($s)));
|
|
||||||
$ar->map("GET", "/home", Action::auth(fn(SessionHandle $s) => getUserController()->home($s)));
|
|
||||||
$ar->map("GET", "/settings", Action::auth(fn(SessionHandle $s) => getUserController()->settings($s)));
|
|
||||||
$ar->map("GET", "/disconnect", Action::auth(fn(MutableSessionHandle $s) => getUserController()->disconnect($s)));
|
|
||||||
|
|
||||||
|
|
||||||
//tactic-related
|
|
||||||
$ar->map("GET", "/tactic/[i:id]/view", Action::auth(fn(int $id, SessionHandle $s) => getVisualizerController()->openVisualizer($id, $s)));
|
|
||||||
$ar->map("GET", "/tactic/[i:id]/edit", Action::auth(fn(int $id, SessionHandle $s) => getEditorController()->openEditor($id, $s)));
|
|
||||||
// don't require an authentication to run this action.
|
|
||||||
// If the user is not connected, the tactic will never save.
|
|
||||||
$ar->map("GET", "/tactic/new", Action::noAuth(fn() => getEditorController()->createNew()));
|
|
||||||
$ar->map("GET", "/tactic/new/plain", Action::noAuth(fn(SessionHandle $s) => getEditorController()->createNewOfKind(CourtType::plain(), $s)));
|
|
||||||
$ar->map("GET", "/tactic/new/half", Action::noAuth(fn(SessionHandle $s) => getEditorController()->createNewOfKind(CourtType::half(), $s)));
|
|
||||||
|
|
||||||
//team-related
|
|
||||||
$ar->map("GET", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->displayCreateTeam($s)));
|
|
||||||
$ar->map("POST", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->submitTeam($_POST, $s)));
|
|
||||||
$ar->map("GET", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->displayListTeamByName($s)));
|
|
||||||
$ar->map("POST", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->listTeamByName($_POST, $s)));
|
|
||||||
$ar->map("GET", "/team/[i:id]", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->displayTeam($id, $s)));
|
|
||||||
$ar->map("GET", "/team/[i:id]/delete", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->deleteTeamById($id, $s)));
|
|
||||||
$ar->map("GET", "/team/[i:id]/addMember", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->displayAddMember($id, $s)));
|
|
||||||
$ar->map("POST", "/team/[i:id]/addMember", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->addMember($id, $_POST, $s)));
|
|
||||||
$ar->map("GET", "/team/[i:idTeam]/remove/[i:idMember]", Action::auth(fn(int $idTeam, int $idMember, SessionHandle $s) => getTeamController()->deleteMember($idTeam, $idMember, $s)));
|
|
||||||
$ar->map("GET", "/team/[i:id]/edit", Action::auth(fn(int $idTeam, SessionHandle $s) => getTeamController()->displayEditTeam($idTeam, $s)));
|
|
||||||
$ar->map("POST", "/team/[i:id]/edit", Action::auth(fn(int $idTeam, SessionHandle $s) => getTeamController()->editTeam($idTeam, $_POST, $s)));
|
|
||||||
|
|
||||||
|
|
||||||
return $ar;
|
|
||||||
}
|
|
||||||
|
|
||||||
function runMatch($match, MutableSessionHandle $session): HttpResponse {
|
|
||||||
global $basePath;
|
|
||||||
if (!$match) {
|
|
||||||
return ViewHttpResponse::twig("error.html.twig", [
|
|
||||||
'failures' => [ValidationFail::notFound("Could not find page {$_SERVER['REQUEST_URI']}.")],
|
|
||||||
], HttpCodes::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
return App::runAction($basePath . '/login', $match['target'], $match['params'], $session);
|
|
||||||
}
|
|
||||||
|
|
||||||
//this is a global variable
|
|
||||||
$basePath = get_base_path();
|
|
||||||
|
|
||||||
App::render(runMatch(getRoutes()->match(), PhpSessionHandle::init()), fn() => getTwig());
|
|
@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use IQBall\Core\Connection;
|
|
||||||
use IQBall\Core\Gateway\AccountGateway;
|
|
||||||
use IQBall\Core\Model\AuthModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init_database($pdo);
|
|
||||||
|
|
||||||
return $pdo;
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
-- drop tables here
|
|
||||||
DROP TABLE IF EXISTS Account;
|
|
||||||
DROP TABLE IF EXISTS Tactic;
|
|
||||||
DROP TABLE IF EXISTS Team;
|
|
||||||
DROP TABLE IF EXISTS User;
|
|
||||||
DROP TABLE IF EXISTS Member;
|
|
||||||
|
|
||||||
CREATE TABLE Admins
|
|
||||||
(
|
|
||||||
id integer PRIMARY KEY REFERENCES Account
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE Account
|
|
||||||
(
|
|
||||||
id integer PRIMARY KEY AUTOINCREMENT,
|
|
||||||
email varchar UNIQUE NOT NULL,
|
|
||||||
username varchar NOT NULL,
|
|
||||||
token varchar UNIQUE NOT NULL,
|
|
||||||
hash varchar NOT NULL,
|
|
||||||
profile_picture varchar NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE Tactic
|
|
||||||
(
|
|
||||||
id integer PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name varchar NOT NULL,
|
|
||||||
creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
owner integer NOT NULL,
|
|
||||||
content varchar DEFAULT '{"players": [], "actions": [], "objects": []}' NOT NULL,
|
|
||||||
court_type varchar CHECK ( court_type IN ('HALF', 'PLAIN')) NOT NULL,
|
|
||||||
FOREIGN KEY (owner) REFERENCES Account
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE Team
|
|
||||||
(
|
|
||||||
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
name varchar NOT NULL,
|
|
||||||
picture varchar NOT NULL,
|
|
||||||
main_color varchar NOT NULL,
|
|
||||||
second_color varchar NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE Member
|
|
||||||
(
|
|
||||||
id_team integer NOT NULL,
|
|
||||||
id_user integer NOT NULL,
|
|
||||||
role text CHECK (role IN ('COACH', 'PLAYER')) NOT NULL,
|
|
||||||
FOREIGN KEY (id_team) REFERENCES Team (id),
|
|
||||||
FOREIGN KEY (id_user) REFERENCES Account (id)
|
|
||||||
);
|
|
@ -1,65 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Api;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use IQBall\Core\Action;
|
|
||||||
use IQBall\Core\Data\Account;
|
|
||||||
use IQBall\Core\Http\HttpCodes;
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
use IQBall\Core\Http\JsonHttpResponse;
|
|
||||||
use IQBall\Core\Validation\ValidationFail;
|
|
||||||
|
|
||||||
class API {
|
|
||||||
public static function consume(HttpResponse $response): void {
|
|
||||||
http_response_code($response->getCode());
|
|
||||||
|
|
||||||
header('Access-Control-Allow-Origin: *');
|
|
||||||
header('Access-Control-Allow-Headers: *');
|
|
||||||
|
|
||||||
|
|
||||||
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>|false $match
|
|
||||||
* @param callable(): Account $tryGetAuthorization function to return account authorisation for the given action (if required)
|
|
||||||
* @return HttpResponse
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public static function handleMatch($match, callable $tryGetAuthorization): HttpResponse {
|
|
||||||
if (!$match) {
|
|
||||||
return new JsonHttpResponse([ValidationFail::notFound("not found")], HttpCodes::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
$action = $match['target'];
|
|
||||||
if (!$action instanceof Action) {
|
|
||||||
throw new Exception("routed action is not an AppAction object.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$account = null;
|
|
||||||
|
|
||||||
if ($action->getAuthType() != Action::NO_AUTH) {
|
|
||||||
$account = call_user_func($tryGetAuthorization);
|
|
||||||
if ($account == null) {
|
|
||||||
return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")], HttpCodes::UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action->getAuthType() == Action::AUTH_ADMIN && !$account->getUser()->isAdmin()) {
|
|
||||||
return new JsonHttpResponse([ValidationFail::unauthorized()], HttpCodes::UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $action->run($match['params'], $account);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Api;
|
|
||||||
|
|
||||||
use IQBall\Core\Control;
|
|
||||||
use IQBall\Core\ControlSchemaErrorResponseFactory;
|
|
||||||
use IQBall\Core\Http\HttpCodes;
|
|
||||||
use IQBall\Core\Http\HttpRequest;
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
use IQBall\Core\Http\JsonHttpResponse;
|
|
||||||
use IQBall\Core\Validation\Validator;
|
|
||||||
|
|
||||||
class APIControl {
|
|
||||||
private static function errorFactory(): ControlSchemaErrorResponseFactory {
|
|
||||||
return new class () implements ControlSchemaErrorResponseFactory {
|
|
||||||
public function apply(array $failures): HttpResponse {
|
|
||||||
return new JsonHttpResponse($failures, HttpCodes::BAD_REQUEST);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs given callback, if the request's payload json validates the given schema.
|
|
||||||
* @param array<string, Validator[]> $schema an array of `fieldName => DefaultValidators` 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 {
|
|
||||||
return Control::runChecked($schema, $run, self::errorFactory());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 => DefaultValidators` 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 {
|
|
||||||
return Control::runCheckedFrom($data, $schema, $run, self::errorFactory());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Api\Controller;
|
|
||||||
|
|
||||||
use IQBall\Api\APIControl;
|
|
||||||
use IQBall\Core\Data\Account;
|
|
||||||
use IQBall\Core\Gateway\AccountGateway;
|
|
||||||
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\DefaultValidators;
|
|
||||||
use IQBall\Core\Validation\ValidationFail;
|
|
||||||
|
|
||||||
class APIAccountsController {
|
|
||||||
private AccountGateway $accounts;
|
|
||||||
private AuthModel $authModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param AuthModel $model
|
|
||||||
* @param AccountGateway $accounts
|
|
||||||
*/
|
|
||||||
public function __construct(AuthModel $model, AccountGateway $accounts) {
|
|
||||||
$this->accounts = $accounts;
|
|
||||||
$this->authModel = $model;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $request
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function listUsers(array $request): HttpResponse {
|
|
||||||
return APIControl::runCheckedFrom($request, [
|
|
||||||
|
|
||||||
'start' => [DefaultValidators::isUnsignedInteger()],
|
|
||||||
'n' => [DefaultValidators::isIntInRange(0, 250)],
|
|
||||||
'search' => [DefaultValidators::lenBetween(0, 256)],
|
|
||||||
], function (HttpRequest $req) {
|
|
||||||
$accounts = $this->accounts->searchAccounts(intval($req['start']), intval($req['n']), $req["search"]);
|
|
||||||
$users = array_map(fn(Account $acc) => $acc->getUser(), $accounts);
|
|
||||||
return new JsonHttpResponse([
|
|
||||||
"users" => $users,
|
|
||||||
"totalCount" => $this->accounts->totalCount(),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $userId
|
|
||||||
* @return HttpResponse given user information.
|
|
||||||
*/
|
|
||||||
public function getUser(int $userId): HttpResponse {
|
|
||||||
$acc = $this->accounts->getAccount($userId);
|
|
||||||
|
|
||||||
if ($acc == null) {
|
|
||||||
return new JsonHttpResponse([ValidationFail::notFound("User not found")], HttpCodes::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JsonHttpResponse($acc->getUser());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addUser(): HttpResponse {
|
|
||||||
return APIControl::runChecked([
|
|
||||||
"username" => [DefaultValidators::name()],
|
|
||||||
"email" => [DefaultValidators::email()],
|
|
||||||
"password" => [DefaultValidators::password()],
|
|
||||||
"isAdmin" => [DefaultValidators::bool()],
|
|
||||||
], function (HttpRequest $req) {
|
|
||||||
$model = new AuthModel($this->accounts);
|
|
||||||
|
|
||||||
$account = $model->register($req["username"], $req["password"], $req["email"]);
|
|
||||||
if ($account == null) {
|
|
||||||
return new JsonHttpResponse([new ValidationFail("already exists", "An account with provided email ")], HttpCodes::FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JsonHttpResponse([
|
|
||||||
"id" => $account->getUser()->getId(),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function removeUsers(): HttpResponse {
|
|
||||||
return APIControl::runChecked([
|
|
||||||
"identifiers" => [DefaultValidators::array(), DefaultValidators::forall(DefaultValidators::isUnsignedInteger())],
|
|
||||||
], function (HttpRequest $req) {
|
|
||||||
$this->accounts->removeAccounts($req["identifiers"]);
|
|
||||||
return HttpResponse::fromCode(HttpCodes::OK);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateUser(int $id): HttpResponse {
|
|
||||||
return APIControl::runChecked([
|
|
||||||
"email" => [DefaultValidators::email()],
|
|
||||||
"username" => [DefaultValidators::name()],
|
|
||||||
"isAdmin" => [DefaultValidators::bool()],
|
|
||||||
], function (HttpRequest $req) use ($id) {
|
|
||||||
$mailAccount = $this->accounts->getAccount($id);
|
|
||||||
if ($mailAccount->getUser()->getId() != $id) {
|
|
||||||
return new JsonHttpResponse([new ValidationFail("email exists", "The provided mail address already exists for another account.")], HttpCodes::FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->authModel->update($id, $req["email"], $req["username"], $req["isAdmin"]);
|
|
||||||
return HttpResponse::fromCode(HttpCodes::OK);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Api\Controller;
|
|
||||||
|
|
||||||
use IQBall\Api\APIControl;
|
|
||||||
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\DefaultValidators;
|
|
||||||
|
|
||||||
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 APIControl::runChecked([
|
|
||||||
"email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)],
|
|
||||||
"password" => [DefaultValidators::password()],
|
|
||||||
], 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,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Api\Controller;
|
|
||||||
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
use IQBall\Core\Http\JsonHttpResponse;
|
|
||||||
|
|
||||||
class APIServerController {
|
|
||||||
private string $basePath;
|
|
||||||
private \PDO $pdo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $basePath
|
|
||||||
* @param \PDO $pdo
|
|
||||||
*/
|
|
||||||
public function __construct(string $basePath, \PDO $pdo) {
|
|
||||||
$this->basePath = $basePath;
|
|
||||||
$this->pdo = $pdo;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function countLines(string $table): int {
|
|
||||||
$stmnt = $this->pdo->prepare("SELECT count(*) FROM $table");
|
|
||||||
$stmnt->execute();
|
|
||||||
$res = $stmnt->fetch(\PDO::FETCH_BOTH);
|
|
||||||
return $res[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return HttpResponse some (useless) information about the server
|
|
||||||
*/
|
|
||||||
public function getServerInfo(): HttpResponse {
|
|
||||||
|
|
||||||
return new JsonHttpResponse([
|
|
||||||
'base_path' => $this->basePath,
|
|
||||||
'date' => (int) gettimeofday(true) * 1000,
|
|
||||||
'database' => [
|
|
||||||
'accounts' => $this->countLines("Account") . " line(s)",
|
|
||||||
'tactics' => $this->countLines("Tactic") . " line(s)",
|
|
||||||
'teams' => $this->countLines("Team") . " line(s)",
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Api\Controller;
|
|
||||||
|
|
||||||
use IQBall\Api\APIControl;
|
|
||||||
use IQBall\Core\Data\Account;
|
|
||||||
use IQBall\Core\Data\TacticInfo;
|
|
||||||
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\DefaultValidators;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 APIControl::runChecked([
|
|
||||||
"name" => [DefaultValidators::lenBetween(1, 50), DefaultValidators::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 APIControl::runChecked([
|
|
||||||
"content" => [],
|
|
||||||
], function (HttpRequest $req) use ($id) {
|
|
||||||
//TODO verify that the account has the rights to update the tactic content
|
|
||||||
if ($fail = $this->model->updateContent($id, json_encode($req["content"]))) {
|
|
||||||
return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST);
|
|
||||||
}
|
|
||||||
return HttpResponse::fromCode(HttpCodes::OK);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $userId
|
|
||||||
* @return HttpResponse given user information.
|
|
||||||
*/
|
|
||||||
public function getUserTactics(int $userId): HttpResponse {
|
|
||||||
$tactics = $this->model->listAllOf($userId);
|
|
||||||
|
|
||||||
$response = array_map(fn(TacticInfo $t) => [
|
|
||||||
'id' => $t->getId(),
|
|
||||||
'name' => $t->getName(),
|
|
||||||
'court' => $t->getCourtType(),
|
|
||||||
'creation_date' => $t->getCreationDate(),
|
|
||||||
|
|
||||||
], $tactics);
|
|
||||||
|
|
||||||
return new JsonHttpResponse($response);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Api\Controller;
|
|
||||||
|
|
||||||
use IQBall\Api\APIControl;
|
|
||||||
use IQBall\Core\Data\Account;
|
|
||||||
use IQBall\Core\Data\Team;
|
|
||||||
use IQBall\Core\Data\TeamInfo;
|
|
||||||
use IQBall\Core\Gateway\TeamGateway;
|
|
||||||
use IQBall\Core\Http\HttpCodes;
|
|
||||||
use IQBall\Core\Http\HttpRequest;
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
use IQBall\Core\Http\JsonHttpResponse;
|
|
||||||
use IQBall\Core\Model\TeamModel;
|
|
||||||
use IQBall\Core\Validation\DefaultValidators;
|
|
||||||
|
|
||||||
class APITeamController {
|
|
||||||
private TeamModel $teamModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TeamModel $teamModel
|
|
||||||
*/
|
|
||||||
public function __construct(TeamModel $teamModel) {
|
|
||||||
$this->teamModel = $teamModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $req_params
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function listTeams(array $req_params): HttpResponse {
|
|
||||||
return APIControl::runCheckedFrom($req_params, [
|
|
||||||
'start' => [DefaultValidators::isUnsignedInteger()],
|
|
||||||
'n' => [DefaultValidators::isUnsignedInteger()],
|
|
||||||
], function (HttpRequest $req) {
|
|
||||||
$teams = $this->teamModel->listAll(intval($req['start']), intval($req['n']));
|
|
||||||
return new JsonHttpResponse([
|
|
||||||
"totalCount" => $this->teamModel->countTeam(),
|
|
||||||
"teams" => $teams,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addTeam(): HttpResponse {
|
|
||||||
return APIControl::runChecked([
|
|
||||||
'name' => [DefaultValidators::name()],
|
|
||||||
'picture' => [DefaultValidators::isURL()],
|
|
||||||
'mainColor' => [DefaultValidators::hexColor()],
|
|
||||||
'secondaryColor' => [DefaultValidators::hexColor()],
|
|
||||||
|
|
||||||
], function (HttpRequest $req) {
|
|
||||||
$this->teamModel->createTeam($req['name'], $req['picture'], $req['mainColor'], $req['secondaryColor']);
|
|
||||||
return HttpResponse::fromCode(HttpCodes::OK);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function deleteTeamSelected(): HttpResponse {
|
|
||||||
return APIControl::runChecked([
|
|
||||||
'teams' => [],
|
|
||||||
], function (HttpRequest $req) {
|
|
||||||
$this->teamModel->deleteTeamSelected($req['teams']);
|
|
||||||
return HttpResponse::fromCode(HttpCodes::OK);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateTeam(int $id): HttpResponse {
|
|
||||||
return APIControl::runChecked([
|
|
||||||
'name' => [DefaultValidators::name()],
|
|
||||||
'picture' => [DefaultValidators::isURL()],
|
|
||||||
'mainColor' => [DefaultValidators::hexColor()],
|
|
||||||
'secondaryColor' => [DefaultValidators::hexColor()],
|
|
||||||
], function (HttpRequest $req) {
|
|
||||||
$this->teamModel->editTeam($req['id'], $req['name'], $req['picture'], $req['mainColor'], $req['secondaryColor']);
|
|
||||||
return HttpResponse::fromCode(HttpCodes::OK);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,275 @@
|
|||||||
|
import {
|
||||||
|
BrowserRouter,
|
||||||
|
Navigate,
|
||||||
|
Outlet,
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
|
useLocation,
|
||||||
|
} from "react-router-dom"
|
||||||
|
|
||||||
|
import { Header } from "./pages/template/Header.tsx"
|
||||||
|
import "./style/app.css"
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
lazy,
|
||||||
|
ReactNode,
|
||||||
|
Suspense,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react"
|
||||||
|
import { BASE } from "./Constants.ts"
|
||||||
|
import { Authentication, Fetcher } from "./app/Fetcher.ts"
|
||||||
|
import { User } from "./model/User.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"))
|
||||||
|
const VisualizerPage = lazy(() => import("./pages/VisualizerPage.tsx"))
|
||||||
|
const Settings = lazy(() => import("./pages/Settings.tsx"))
|
||||||
|
|
||||||
|
const TOKEN_REFRESH_INTERVAL_MS = 60 * 1000
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
function suspense(node: ReactNode) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<p>Loading, please wait...</p>}>
|
||||||
|
{node}
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = useMemo(() => new Fetcher(getStoredAuthentication()), [])
|
||||||
|
const [user, setUser] = useState<User | null>(null)
|
||||||
|
|
||||||
|
const handleAuthSuccess = useCallback(
|
||||||
|
async (auth: Authentication) => {
|
||||||
|
fetcher.updateAuthentication(auth)
|
||||||
|
const user = await fetchUser(fetcher)
|
||||||
|
setUser(user)
|
||||||
|
storeAuthentication(auth)
|
||||||
|
},
|
||||||
|
[fetcher],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetcher.fetchAPIGet("auth/keep-alive")
|
||||||
|
}, TOKEN_REFRESH_INTERVAL_MS)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [fetcher])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="app">
|
||||||
|
<FetcherContext.Provider value={fetcher}>
|
||||||
|
<SignedInUserContext.Provider
|
||||||
|
value={{
|
||||||
|
user,
|
||||||
|
setUser,
|
||||||
|
}}>
|
||||||
|
<BrowserRouter basename={BASE}>
|
||||||
|
<Outlet />
|
||||||
|
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path={"/login"}
|
||||||
|
element={suspense(
|
||||||
|
<LoginPage onSuccess={handleAuthSuccess} />,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/register"}
|
||||||
|
element={suspense(
|
||||||
|
<RegisterPage
|
||||||
|
onSuccess={handleAuthSuccess}
|
||||||
|
/>,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path={"/"} element={suspense(<AppLayout />)}>
|
||||||
|
<Route
|
||||||
|
path={"/"}
|
||||||
|
element={suspense(
|
||||||
|
<LoggedInPage>
|
||||||
|
<HomePage />
|
||||||
|
</LoggedInPage>,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/home"}
|
||||||
|
element={suspense(
|
||||||
|
<LoggedInPage>
|
||||||
|
<HomePage />
|
||||||
|
</LoggedInPage>,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/settings"}
|
||||||
|
element={suspense(
|
||||||
|
<LoggedInPage>
|
||||||
|
<Settings />
|
||||||
|
</LoggedInPage>,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/team/new"}
|
||||||
|
element={suspense(<CreateTeamPage />)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/team/:teamId"}
|
||||||
|
element={suspense(
|
||||||
|
<LoggedInPage>
|
||||||
|
<TeamPanelPage />
|
||||||
|
</LoggedInPage>,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/tactic/new"}
|
||||||
|
element={suspense(<NewTacticPage />)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/tactic/:tacticId/edit"}
|
||||||
|
element={suspense(
|
||||||
|
<LoggedInPage>
|
||||||
|
<Editor guestMode={false} />
|
||||||
|
</LoggedInPage>,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/tactic/:tacticId/view"}
|
||||||
|
element={suspense(
|
||||||
|
<LoggedInPage>
|
||||||
|
<VisualizerPage guestMode={false} />
|
||||||
|
</LoggedInPage>,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/tactic/view-guest"}
|
||||||
|
element={suspense(
|
||||||
|
<VisualizerPage guestMode={true} />,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={"/tactic/edit-guest"}
|
||||||
|
element={suspense(
|
||||||
|
<Editor guestMode={true} />,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={"*"}
|
||||||
|
element={suspense(<NotFoundPage />)}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</SignedInUserContext.Provider>
|
||||||
|
</FetcherContext.Provider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUser(fetcher: Fetcher): Promise<User> {
|
||||||
|
const response = await fetcher.fetchAPIGet("user")
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw Error(
|
||||||
|
"Could not retrieve user information : " + (await response.text()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_AUTH_KEY = "token"
|
||||||
|
|
||||||
|
function getStoredAuthentication(): Authentication {
|
||||||
|
const storedUser = localStorage.getItem(STORAGE_AUTH_KEY)
|
||||||
|
return storedUser == null ? null : JSON.parse(storedUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeAuthentication(auth: Authentication) {
|
||||||
|
localStorage.setItem(STORAGE_AUTH_KEY, JSON.stringify(auth))
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoggedInPageProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UserFetchingState {
|
||||||
|
FETCHING,
|
||||||
|
FETCHED,
|
||||||
|
ERROR,
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoggedInPage({ children }: LoggedInPageProps) {
|
||||||
|
const [user, setUser] = useUser()
|
||||||
|
const fetcher = useAppFetcher()
|
||||||
|
const [userFetchingState, setUserFetchingState] = useState(
|
||||||
|
user === null ? UserFetchingState.FETCHING : UserFetchingState.FETCHED,
|
||||||
|
)
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function initUser() {
|
||||||
|
try {
|
||||||
|
const user = await fetchUser(fetcher)
|
||||||
|
setUser(user)
|
||||||
|
setUserFetchingState(UserFetchingState.FETCHED)
|
||||||
|
} catch (e) {
|
||||||
|
setUserFetchingState(UserFetchingState.ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userFetchingState === UserFetchingState.FETCHING) initUser()
|
||||||
|
}, [fetcher, setUser, userFetchingState])
|
||||||
|
|
||||||
|
switch (userFetchingState) {
|
||||||
|
case UserFetchingState.ERROR:
|
||||||
|
return (
|
||||||
|
<Navigate
|
||||||
|
to={"/login"}
|
||||||
|
replace
|
||||||
|
state={{ from: location.pathname }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case UserFetchingState.FETCHED:
|
||||||
|
return children
|
||||||
|
case UserFetchingState.FETCHING:
|
||||||
|
return <p>Fetching user...</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserContext {
|
||||||
|
user: User | null
|
||||||
|
setUser: (user: User | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignedInUserContext = createContext<UserContext | null>(null)
|
||||||
|
const FetcherContext = createContext(new Fetcher())
|
||||||
|
|
||||||
|
export function useAppFetcher() {
|
||||||
|
return useContext(FetcherContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUser(): [User | null, (user: User | null) => void] {
|
||||||
|
const { user, setUser } = useContext(SignedInUserContext)!
|
||||||
|
return [user, setUser]
|
||||||
|
}
|
@ -1,98 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\App;
|
|
||||||
|
|
||||||
use IQBall\App\Session\MutableSessionHandle;
|
|
||||||
use IQBall\Core\Action;
|
|
||||||
use IQBall\Core\Http\HttpCodes;
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
use IQBall\Core\Http\JsonHttpResponse;
|
|
||||||
use IQBall\Core\Validation\ValidationFail;
|
|
||||||
use Twig\Environment;
|
|
||||||
use Twig\Error\LoaderError;
|
|
||||||
use Twig\Error\RuntimeError;
|
|
||||||
use Twig\Error\SyntaxError;
|
|
||||||
|
|
||||||
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->getAuthType() != Action::NO_AUTH) {
|
|
||||||
$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::redirectAbsolute($authRoute);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action->getAuthType() == Action::AUTH_ADMIN && !$account->getUser()->isAdmin()) {
|
|
||||||
return new JsonHttpResponse([ValidationFail::unauthorized()], HttpCodes::UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return $action->run($params, $session);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\App;
|
|
||||||
|
|
||||||
use IQBall\Core\Control;
|
|
||||||
use IQBall\Core\ControlSchemaErrorResponseFactory;
|
|
||||||
use IQBall\Core\Http\HttpCodes;
|
|
||||||
use IQBall\Core\Http\HttpRequest;
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
use IQBall\Core\Validation\Validator;
|
|
||||||
|
|
||||||
class AppControl {
|
|
||||||
private static function errorFactory(): ControlSchemaErrorResponseFactory {
|
|
||||||
return new class () implements ControlSchemaErrorResponseFactory {
|
|
||||||
public function apply(array $failures): HttpResponse {
|
|
||||||
return ViewHttpResponse::twig("error.html.twig", ['failures' => $failures], HttpCodes::BAD_REQUEST);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs given callback, if the request's payload json validates the given schema.
|
|
||||||
* @param array<string, Validator[]> $schema an array of `fieldName => DefaultValidators` 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 {
|
|
||||||
return Control::runChecked($schema, $run, self::errorFactory());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 => DefaultValidators` 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 {
|
|
||||||
return Control::runCheckedFrom($data, $schema, $run, self::errorFactory());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\App\Controller;
|
|
||||||
|
|
||||||
use IQBall\App\Session\MutableSessionHandle;
|
|
||||||
use IQBall\App\ViewHttpResponse;
|
|
||||||
use IQBall\Core\Http\HttpRequest;
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
use IQBall\Core\Model\AuthModel;
|
|
||||||
|
|
||||||
use IQBall\Core\Validation\DefaultValidators;
|
|
||||||
use IQBall\Core\Validation\FieldValidationFail;
|
|
||||||
|
|
||||||
class AuthController {
|
|
||||||
private AuthModel $model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param AuthModel $model
|
|
||||||
*/
|
|
||||||
public function __construct(AuthModel $model) {
|
|
||||||
$this->model = $model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function displayRegister(): HttpResponse {
|
|
||||||
return ViewHttpResponse::twig("display_register.html.twig", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* registers given account
|
|
||||||
* @param mixed[] $request
|
|
||||||
* @param MutableSessionHandle $session
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function register(array $request, MutableSessionHandle $session): HttpResponse {
|
|
||||||
$fails = [];
|
|
||||||
$request = HttpRequest::from($request, $fails, [
|
|
||||||
"username" => [DefaultValidators::name(), DefaultValidators::lenBetween(2, 32)],
|
|
||||||
"password" => [DefaultValidators::password()],
|
|
||||||
"confirmpassword" => [DefaultValidators::password()],
|
|
||||||
"email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!empty($fails)) {
|
|
||||||
if (!(in_array($request['username'], $fails)) or !(in_array($request['email'], $fails))) {
|
|
||||||
return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails,'username' => $request['username'],'email' => $request['email']]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request["password"] != $request['confirmpassword']) {
|
|
||||||
$fails[] = new FieldValidationFail("confirmpassword", "Le mot de passe et la confirmation ne sont pas les mêmes.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$account = $this->model->register($request['username'], $request["password"], $request['email']);
|
|
||||||
|
|
||||||
if (!$account) {
|
|
||||||
$fails[] = new FieldValidationFail("email", "L'email existe déjà");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($fails)) {
|
|
||||||
return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]);
|
|
||||||
}
|
|
||||||
$session->setAccount($account);
|
|
||||||
|
|
||||||
$target_url = $session->getInitialTarget();
|
|
||||||
if ($target_url != null) {
|
|
||||||
return HttpResponse::redirectAbsolute($target_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return HttpResponse::redirect("/home");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function displayLogin(): HttpResponse {
|
|
||||||
return ViewHttpResponse::twig("display_login.html.twig", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* logins given account credentials
|
|
||||||
* @param mixed[] $request
|
|
||||||
* @param MutableSessionHandle $session
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function login(array $request, MutableSessionHandle $session): HttpResponse {
|
|
||||||
$fails = [];
|
|
||||||
$account = $this->model->login($request['email'], $request['password'], $fails);
|
|
||||||
if (!empty($fails)) {
|
|
||||||
return ViewHttpResponse::twig("display_login.html.twig", ['fails' => $fails]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$session->setAccount($account);
|
|
||||||
|
|
||||||
$target_url = $session->getInitialTarget();
|
|
||||||
$session->setInitialTarget(null);
|
|
||||||
if ($target_url != null) {
|
|
||||||
return HttpResponse::redirectAbsolute($target_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return HttpResponse::redirect("/home");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\App\Controller;
|
|
||||||
|
|
||||||
use IQBall\App\Session\SessionHandle;
|
|
||||||
use IQBall\App\Validator\TacticValidator;
|
|
||||||
use IQBall\App\ViewHttpResponse;
|
|
||||||
use IQBall\Core\Data\CourtType;
|
|
||||||
use IQBall\Core\Data\TacticInfo;
|
|
||||||
use IQBall\Core\Http\HttpCodes;
|
|
||||||
use IQBall\Core\Model\TacticModel;
|
|
||||||
use IQBall\Core\Validation\ValidationFail;
|
|
||||||
|
|
||||||
class EditorController {
|
|
||||||
private TacticModel $model;
|
|
||||||
|
|
||||||
public function __construct(TacticModel $model) {
|
|
||||||
$this->model = $model;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TacticInfo $tactic
|
|
||||||
* @return ViewHttpResponse the editor view for given tactic
|
|
||||||
*/
|
|
||||||
private function openEditorFor(TacticInfo $tactic): ViewHttpResponse {
|
|
||||||
return ViewHttpResponse::react("views/Editor.tsx", [
|
|
||||||
"id" => $tactic->getId(),
|
|
||||||
"name" => $tactic->getName(),
|
|
||||||
"content" => $tactic->getContent(),
|
|
||||||
"courtType" => $tactic->getCourtType()->name(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createNew(): ViewHttpResponse {
|
|
||||||
return ViewHttpResponse::react("views/NewTacticPanel.tsx", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return ViewHttpResponse the editor view for a test tactic.
|
|
||||||
*/
|
|
||||||
private function openTestEditor(CourtType $courtType): ViewHttpResponse {
|
|
||||||
return ViewHttpResponse::react("views/Editor.tsx", [
|
|
||||||
"id" => -1, //-1 id means that the editor will not support saves
|
|
||||||
"name" => TacticModel::TACTIC_DEFAULT_NAME,
|
|
||||||
"content" => '{"players": [], "objects": [], "actions": []}',
|
|
||||||
"courtType" => $courtType->name(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates a new empty tactic, with default name
|
|
||||||
* If the given session does not contain a connected account,
|
|
||||||
* open a test editor.
|
|
||||||
* @param SessionHandle $session
|
|
||||||
* @param CourtType $type
|
|
||||||
* @return ViewHttpResponse the editor view
|
|
||||||
*/
|
|
||||||
public function createNewOfKind(CourtType $type, SessionHandle $session): ViewHttpResponse {
|
|
||||||
|
|
||||||
$action = $session->getAccount();
|
|
||||||
|
|
||||||
if ($action == null) {
|
|
||||||
return $this->openTestEditor($type);
|
|
||||||
}
|
|
||||||
|
|
||||||
$tactic = $this->model->makeNewDefault($session->getAccount()->getUser()->getId(), $type);
|
|
||||||
return $this->openEditorFor($tactic);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns an editor view for a given tactic
|
|
||||||
* @param int $id the targeted tactic identifier
|
|
||||||
* @param SessionHandle $session
|
|
||||||
* @return ViewHttpResponse
|
|
||||||
*/
|
|
||||||
public function openEditor(int $id, SessionHandle $session): ViewHttpResponse {
|
|
||||||
$tactic = $this->model->get($id);
|
|
||||||
|
|
||||||
$failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getUser()->getId());
|
|
||||||
|
|
||||||
if ($failure != null) {
|
|
||||||
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->openEditorFor($tactic);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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\DefaultValidators;
|
|
||||||
|
|
||||||
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" => [DefaultValidators::lenBetween(1, 32), DefaultValidators::nameWithSpaces()],
|
|
||||||
"main_color" => [DefaultValidators::hexColor()],
|
|
||||||
"second_color" => [DefaultValidators::hexColor()],
|
|
||||||
"picture" => [DefaultValidators::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" => [DefaultValidators::lenBetween(1, 32), DefaultValidators::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" => [DefaultValidators::email(), DefaultValidators::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" => [DefaultValidators::lenBetween(1, 32), DefaultValidators::nameWithSpaces()],
|
|
||||||
"main_color" => [DefaultValidators::hexColor()],
|
|
||||||
"second_color" => [DefaultValidators::hexColor()],
|
|
||||||
"picture" => [DefaultValidators::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,66 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\App\Controller;
|
|
||||||
|
|
||||||
use IQBall\App\Session\MutableSessionHandle;
|
|
||||||
use IQBall\App\Session\SessionHandle;
|
|
||||||
use IQBall\App\ViewHttpResponse;
|
|
||||||
use IQBall\Core\Gateway\AccountGateway;
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
use IQBall\Core\Model\TacticModel;
|
|
||||||
use IQBall\Core\Model\TeamModel;
|
|
||||||
|
|
||||||
class UserController {
|
|
||||||
private TacticModel $tactics;
|
|
||||||
private ?TeamModel $teams;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TacticModel $tactics
|
|
||||||
* @param TeamModel|null $teams
|
|
||||||
*/
|
|
||||||
public function __construct(TacticModel $tactics, ?TeamModel $teams = null) {
|
|
||||||
$this->tactics = $tactics;
|
|
||||||
$this->teams = $teams;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param SessionHandle $session
|
|
||||||
* @return ViewHttpResponse the home page view
|
|
||||||
*/
|
|
||||||
public function home(SessionHandle $session): ViewHttpResponse {
|
|
||||||
$limitNbTactics = 5;
|
|
||||||
|
|
||||||
$user = $session->getAccount()->getUser();
|
|
||||||
|
|
||||||
$lastTactics = $this->tactics->getLast($limitNbTactics, $user->getId());
|
|
||||||
$allTactics = $this->tactics->getAll($user->getId());
|
|
||||||
$name = $user->getName();
|
|
||||||
|
|
||||||
if ($this->teams != null) {
|
|
||||||
$teams = $this->teams->getAll($user->getId());
|
|
||||||
} else {
|
|
||||||
$teams = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ViewHttpResponse::react("views/Home.tsx", [
|
|
||||||
"lastTactics" => $lastTactics,
|
|
||||||
"allTactics" => $allTactics,
|
|
||||||
"teams" => $teams,
|
|
||||||
"username" => $name,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return ViewHttpResponse account settings page
|
|
||||||
*/
|
|
||||||
public function settings(SessionHandle $session): ViewHttpResponse {
|
|
||||||
return ViewHttpResponse::react("views/Settings.tsx", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function disconnect(MutableSessionHandle $session): HttpResponse {
|
|
||||||
$session->destroy();
|
|
||||||
return HttpResponse::redirect("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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,123 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.consentement{
|
|
||||||
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>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" value="{{ 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" value="{{ email }}"required><br><br>
|
|
||||||
<label class="consentement">
|
|
||||||
<input type="checkbox" name="consentement" required>
|
|
||||||
En cochant cette case, j'accepte que mes données personnelles, tel que mon adresse e-mail, soient collectées et traitées conformément à la politique de confidentialité de Sportify.
|
|
||||||
</label><br>
|
|
||||||
<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,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core;
|
|
||||||
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represent an action.
|
|
||||||
* @template S session
|
|
||||||
*/
|
|
||||||
class Action {
|
|
||||||
public const NO_AUTH = 1;
|
|
||||||
public const AUTH_USER = 2;
|
|
||||||
public const AUTH_ADMIN = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var callable(mixed[], S): HttpResponse $action action to call
|
|
||||||
*/
|
|
||||||
protected $action;
|
|
||||||
|
|
||||||
private int $authType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callable(mixed[], S): HttpResponse $action
|
|
||||||
*/
|
|
||||||
protected function __construct(callable $action, int $authType) {
|
|
||||||
$this->action = $action;
|
|
||||||
$this->authType = $authType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAuthType(): int {
|
|
||||||
return $this->authType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, self::NO_AUTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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, self::AUTH_USER);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callable(mixed[], S): HttpResponse $action
|
|
||||||
* @return Action<S> an action that does require to have an authorization, and to be an administrator.
|
|
||||||
*/
|
|
||||||
public static function admin(callable $action): Action {
|
|
||||||
return new Action($action, self::AUTH_ADMIN);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core;
|
|
||||||
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
class Connection {
|
|
||||||
private PDO $pdo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param PDO $pdo
|
|
||||||
*/
|
|
||||||
public function __construct(PDO $pdo) {
|
|
||||||
$this->pdo = $pdo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function lastInsertId(): string {
|
|
||||||
return $this->pdo->lastInsertId();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* execute a request
|
|
||||||
* @param string $query
|
|
||||||
* @param array<string, array<mixed, int>> $args
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function exec(string $query, array $args) {
|
|
||||||
$stmnt = $this->prep($query, $args);
|
|
||||||
$stmnt->execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a request, and return the returned rows
|
|
||||||
* @param string $query the SQL request
|
|
||||||
* @param array<string, array<mixed, int>> $args an array containing the arguments label, value and type: ex: `[":label" => [$value, PDO::PARAM_TYPE]`
|
|
||||||
* @return array<string, mixed>[] the returned rows of the request
|
|
||||||
*/
|
|
||||||
public function fetch(string $query, array $args): array {
|
|
||||||
$stmnt = $this->prep($query, $args);
|
|
||||||
$stmnt->execute();
|
|
||||||
return $stmnt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $query
|
|
||||||
* @param array<string, array<mixed, int>> $args
|
|
||||||
* @return \PDOStatement
|
|
||||||
*/
|
|
||||||
private function prep(string $query, array $args): \PDOStatement {
|
|
||||||
$stmnt = $this->pdo->prepare($query);
|
|
||||||
foreach ($args as $name => $value) {
|
|
||||||
$stmnt->bindValue($name, $value[0], $value[1]);
|
|
||||||
}
|
|
||||||
return $stmnt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function prepare(string $query): \PDOStatement {
|
|
||||||
return $this->pdo->prepare($query);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core;
|
|
||||||
|
|
||||||
use IQBall\Core\Http\HttpCodes;
|
|
||||||
use IQBall\Core\Http\HttpRequest;
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
use IQBall\Core\Http\JsonHttpResponse;
|
|
||||||
use IQBall\Core\Validation\ValidationFail;
|
|
||||||
use IQBall\Core\Validation\Validator;
|
|
||||||
|
|
||||||
class Control {
|
|
||||||
/**
|
|
||||||
* Runs given callback, if the request's payload json validates the given schema.
|
|
||||||
* @param array<string, Validator[]> $schema an array of `fieldName => DefaultValidators` 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.
|
|
||||||
* @param ControlSchemaErrorResponseFactory $errorFactory an error factory to use if the request does not validate the required schema
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public static function runChecked(array $schema, callable $run, ControlSchemaErrorResponseFactory $errorFactory): 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 $errorFactory->apply([$fail]);
|
|
||||||
|
|
||||||
}
|
|
||||||
$payload = get_object_vars($payload_obj);
|
|
||||||
return self::runCheckedFrom($payload, $schema, $run, $errorFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 => DefaultValidators` 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.
|
|
||||||
* @param ControlSchemaErrorResponseFactory $errorFactory an error factory to use if the request does not validate the required schema
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public static function runCheckedFrom(array $data, array $schema, callable $run, ControlSchemaErrorResponseFactory $errorFactory): HttpResponse {
|
|
||||||
$fails = [];
|
|
||||||
$request = HttpRequest::from($data, $fails, $schema);
|
|
||||||
|
|
||||||
if (!empty($fails)) {
|
|
||||||
return $errorFactory->apply($fails);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return call_user_func_array($run, [$request]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core;
|
|
||||||
|
|
||||||
use IQBall\Core\Http\HttpResponse;
|
|
||||||
use IQBall\Core\Validation\ValidationFail;
|
|
||||||
|
|
||||||
interface ControlSchemaErrorResponseFactory {
|
|
||||||
/**
|
|
||||||
* @param ValidationFail[] $failures
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function apply(array $failures): HttpResponse;
|
|
||||||
}
|
|
@ -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,50 +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,84 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Data;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var bool true if the user is an administrator
|
|
||||||
*/
|
|
||||||
private bool $isAdmin;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $email
|
|
||||||
* @param string $name
|
|
||||||
* @param int $id
|
|
||||||
* @param string $profilePicture
|
|
||||||
* @param bool $isAdmin
|
|
||||||
*/
|
|
||||||
public function __construct(string $email, string $name, int $id, string $profilePicture, bool $isAdmin) {
|
|
||||||
$this->email = $email;
|
|
||||||
$this->name = $name;
|
|
||||||
$this->id = $id;
|
|
||||||
$this->profilePicture = $profilePicture;
|
|
||||||
$this->isAdmin = $isAdmin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isAdmin(): bool {
|
|
||||||
return $this->isAdmin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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,180 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace IQBall\Core\Gateway;
|
|
||||||
|
|
||||||
use IQBall\Core\Connection;
|
|
||||||
use IQBall\Core\Data\Account;
|
|
||||||
use IQBall\Core\Data\User;
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
class AccountGateway {
|
|
||||||
private Connection $con;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Connection $con
|
|
||||||
*/
|
|
||||||
public function __construct(Connection $con) {
|
|
||||||
$this->con = $con;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function insertAccount(string $name, string $email, string $token, string $hash, string $profilePicture): int {
|
|
||||||
$this->con->exec("INSERT INTO Account(username, hash, email, token,profile_picture) VALUES (:username,:hash,:email,:token,:profile_pic)", [
|
|
||||||
':username' => [$name, PDO::PARAM_STR],
|
|
||||||
':hash' => [$hash, PDO::PARAM_STR],
|
|
||||||
':email' => [$email, PDO::PARAM_STR],
|
|
||||||
':token' => [$token, PDO::PARAM_STR],
|
|
||||||
':profile_pic' => [$profilePicture, PDO::PARAM_STR],
|
|
||||||
]);
|
|
||||||
return intval($this->con->lastInsertId());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateAccount(int $id, string $name, string $email, string $token, bool $isAdmin): void {
|
|
||||||
$this->con->exec("UPDATE Account SET username = :username, email = :email, token = :token WHERE id = :id", [
|
|
||||||
':username' => [$name, PDO::PARAM_STR],
|
|
||||||
':email' => [$email, PDO::PARAM_STR],
|
|
||||||
':token' => [$token, PDO::PARAM_STR],
|
|
||||||
':id' => [$id, PDO::PARAM_INT],
|
|
||||||
]);
|
|
||||||
$this->setIsAdmin($id, $isAdmin);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isAdmin(int $id): bool {
|
|
||||||
$stmnt = $this->con->prepare("SELECT * FROM Admins WHERE id = :id");
|
|
||||||
$stmnt->bindValue(':id', $id, PDO::PARAM_INT);
|
|
||||||
$stmnt->execute();
|
|
||||||
$result = $stmnt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
return !empty($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* promote or demote a user to server administrator
|
|
||||||
* @param int $id
|
|
||||||
* @param bool $isAdmin true to promote, false to demote
|
|
||||||
* @return bool true if the given user exists
|
|
||||||
*/
|
|
||||||
public function setIsAdmin(int $id, bool $isAdmin): bool {
|
|
||||||
if ($isAdmin) {
|
|
||||||
$stmnt = $this->con->prepare("INSERT INTO Admins VALUES(:id)");
|
|
||||||
} else {
|
|
||||||
$stmnt = $this->con->prepare("DELETE FROM Admins WHERE id = :id");
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmnt->bindValue(':id', $id);
|
|
||||||
$stmnt->execute();
|
|
||||||
|
|
||||||
return $stmnt->rowCount() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $email
|
|
||||||
* @return array<string, mixed>|null
|
|
||||||
*/
|
|
||||||
private function getRowsFromMail(string $email): ?array {
|
|
||||||
return $this->con->fetch("SELECT * FROM Account WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]])[0] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $email
|
|
||||||
* @return string|null the hashed user's password, or null if the given mail does not exist
|
|
||||||
*/
|
|
||||||
public function getHash(string $email): ?string {
|
|
||||||
$results = $this->getRowsFromMail($email);
|
|
||||||
if ($results == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return $results['hash'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $email
|
|
||||||
* @return bool true if the given email exists in the database
|
|
||||||
*/
|
|
||||||
public function exists(string $email): bool {
|
|
||||||
return $this->getRowsFromMail($email) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $email
|
|
||||||
* @return Account|null
|
|
||||||
*/
|
|
||||||
public function getAccountFromMail(string $email): ?Account {
|
|
||||||
$acc = $this->getRowsFromMail($email);
|
|
||||||
if (empty($acc)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Account($acc["token"], new User($email, $acc["username"], $acc["id"], $acc["profile_picture"], $this->isAdmin($acc["id"])));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $token get an account from given token
|
|
||||||
* @return Account|null
|
|
||||||
*/
|
|
||||||
public function getAccountFromToken(string $token): ?Account {
|
|
||||||
$stmnt = $this->con->prepare("SELECT * FROM Account WHERE token = :token");
|
|
||||||
$stmnt->bindValue(':token', $token);
|
|
||||||
return $this->getAccountFrom($stmnt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $id get an account from given identifier
|
|
||||||
* @return Account|null
|
|
||||||
*/
|
|
||||||
public function getAccount(int $id): ?Account {
|
|
||||||
$stmnt = $this->con->prepare("SELECT * FROM Account WHERE id = :id");
|
|
||||||
$stmnt->bindValue(':id', $id);
|
|
||||||
return $this->getAccountFrom($stmnt);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getAccountFrom(\PDOStatement $stmnt): ?Account {
|
|
||||||
$stmnt->execute();
|
|
||||||
$acc = $stmnt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if ($acc == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $this->isAdmin($acc["id"])));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a list containing n accounts from a given starting index
|
|
||||||
*
|
|
||||||
* @param integer $n the number of accounts to retrieve
|
|
||||||
* @param int $start starting index of the list content
|
|
||||||
* @return Account[]
|
|
||||||
*/
|
|
||||||
public function searchAccounts(int $start, int $n, ?string $searchString): array {
|
|
||||||
$res = $this->con->fetch(
|
|
||||||
"SELECT * FROM Account WHERE username LIKE '%' || :search || '%' OR email LIKE '%' || :search || '%' ORDER BY username, email LIMIT :offset, :n",
|
|
||||||
[
|
|
||||||
":offset" => [$start, PDO::PARAM_INT],
|
|
||||||
":n" => [$n, PDO::PARAM_INT],
|
|
||||||
":search" => [$searchString ?? "", PDO::PARAM_STR],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
return array_map(fn(array $acc) => new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $this->isAdmin($acc["id"]))), $res);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns the total amount of accounts in the database
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function totalCount(): int {
|
|
||||||
return $this->con->fetch("SELECT count(*) FROM Account", [])[0]['count(*)'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* remove a bunch of account identifiers
|
|
||||||
* @param int[] $accountIds
|
|
||||||
*/
|
|
||||||
public function removeAccounts(array $accountIds): void {
|
|
||||||
foreach ($accountIds as $accountId) {
|
|
||||||
$this->con->fetch("DELETE FROM Account WHERE id = :accountId", [
|
|
||||||
":accountId" => [$accountId, PDO::PARAM_INT],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue