pull/17/head
commit
35d0370564
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$finder = (new PhpCsFixer\Finder())->in(__DIR__);
|
||||||
|
|
||||||
|
return (new PhpCsFixer\Config())
|
||||||
|
->setRules([
|
||||||
|
'@PER-CS' => true,
|
||||||
|
'@PHP74Migration' => true,
|
||||||
|
'array_syntax' => ['syntax' => 'short'],
|
||||||
|
'braces_position' => [
|
||||||
|
'classes_opening_brace' => 'same_line',
|
||||||
|
'functions_opening_brace' => 'same_line'
|
||||||
|
]
|
||||||
|
])
|
||||||
|
->setIndent(" ")
|
||||||
|
->setFinder($finder);
|
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": false
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
@startuml
|
|
||||||
|
|
||||||
class Connexion
|
|
||||||
|
|
||||||
class Modele
|
|
||||||
|
|
||||||
class Account
|
|
||||||
|
|
||||||
class AccountGateway
|
|
||||||
|
|
||||||
|
|
||||||
@enduml
|
|
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
## verify php and typescript types
|
||||||
|
|
||||||
|
echo "formatting php typechecking"
|
||||||
|
vendor/bin/php-cs-fixer fix
|
||||||
|
|
||||||
|
echo "formatting typescript typechecking"
|
||||||
|
npm run format
|
@ -1,4 +1,4 @@
|
|||||||
/**
|
/**
|
||||||
* This constant defines the API endpoint.
|
* This constant defines the API endpoint.
|
||||||
*/
|
*/
|
||||||
export const API = import.meta.env.VITE_API_ENDPOINT;
|
export const API = import.meta.env.VITE_API_ENDPOINT
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 427 B |
@ -0,0 +1,72 @@
|
|||||||
|
import { ReactElement, useRef } from "react"
|
||||||
|
import Draggable from "react-draggable"
|
||||||
|
|
||||||
|
export interface RackProps<E extends { key: string | number }> {
|
||||||
|
id: string
|
||||||
|
objects: E[]
|
||||||
|
onChange: (objects: E[]) => void
|
||||||
|
canDetach: (ref: HTMLDivElement) => boolean
|
||||||
|
onElementDetached: (ref: HTMLDivElement, el: E) => void
|
||||||
|
render: (e: E) => ReactElement
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RackItemProps<E extends { key: string | number }> {
|
||||||
|
item: E
|
||||||
|
onTryDetach: (ref: HTMLDivElement, el: E) => void
|
||||||
|
render: (e: E) => ReactElement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container of draggable objects
|
||||||
|
* */
|
||||||
|
export function Rack<E extends { key: string | number }>({
|
||||||
|
id,
|
||||||
|
objects,
|
||||||
|
onChange,
|
||||||
|
canDetach,
|
||||||
|
onElementDetached,
|
||||||
|
render,
|
||||||
|
}: RackProps<E>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
}}>
|
||||||
|
{objects.map((element) => (
|
||||||
|
<RackItem
|
||||||
|
key={element.key}
|
||||||
|
item={element}
|
||||||
|
render={render}
|
||||||
|
onTryDetach={(ref, element) => {
|
||||||
|
if (!canDetach(ref)) return
|
||||||
|
|
||||||
|
const index = objects.findIndex(
|
||||||
|
(o) => o.key === element.key,
|
||||||
|
)
|
||||||
|
onChange(objects.toSpliced(index, 1))
|
||||||
|
|
||||||
|
onElementDetached(ref, element)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RackItem<E extends { key: string | number }>({
|
||||||
|
item,
|
||||||
|
onTryDetach,
|
||||||
|
render,
|
||||||
|
}: RackItemProps<E>) {
|
||||||
|
const divRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable
|
||||||
|
position={{ x: 0, y: 0 }}
|
||||||
|
nodeRef={divRef}
|
||||||
|
onStop={() => onTryDetach(divRef.current!, item)}>
|
||||||
|
<div ref={divRef}>{render(item)}</div>
|
||||||
|
</Draggable>
|
||||||
|
)
|
||||||
|
}
|
@ -1,28 +1,32 @@
|
|||||||
import React, {CSSProperties, useRef, useState} from "react";
|
import React, { CSSProperties, useRef, useState } from "react"
|
||||||
import "../style/title_input.css";
|
import "../style/title_input.css"
|
||||||
|
|
||||||
export interface TitleInputOptions {
|
export interface TitleInputOptions {
|
||||||
style: CSSProperties,
|
style: CSSProperties
|
||||||
default_value: string,
|
default_value: string
|
||||||
on_validated: (a: string) => void
|
on_validated: (a: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TitleInput({style, default_value, on_validated}: TitleInputOptions) {
|
export default function TitleInput({
|
||||||
const [value, setValue] = useState(default_value);
|
style,
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
default_value,
|
||||||
|
on_validated,
|
||||||
|
}: TitleInputOptions) {
|
||||||
|
const [value, setValue] = useState(default_value)
|
||||||
|
const ref = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input className="title_input"
|
<input
|
||||||
ref={ref}
|
className="title_input"
|
||||||
style={style}
|
ref={ref}
|
||||||
type="text"
|
style={style}
|
||||||
value={value}
|
type="text"
|
||||||
onChange={event => setValue(event.target.value)}
|
value={value}
|
||||||
onBlur={_ => on_validated(value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
onKeyDown={event => {
|
onBlur={(_) => on_validated(value)}
|
||||||
if (event.key == 'Enter')
|
onKeyDown={(event) => {
|
||||||
ref.current?.blur();
|
if (event.key == "Enter") ref.current?.blur()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import CourtSvg from "../../assets/basketball_court.svg?react"
|
||||||
|
import "../../style/basket_court.css"
|
||||||
|
import { useRef } from "react"
|
||||||
|
import CourtPlayer from "./CourtPlayer"
|
||||||
|
import { Player } from "../../data/Player"
|
||||||
|
|
||||||
|
export interface BasketCourtProps {
|
||||||
|
players: Player[]
|
||||||
|
onPlayerRemove: (p: Player) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BasketCourt({ players, onPlayerRemove }: BasketCourtProps) {
|
||||||
|
return (
|
||||||
|
<div id="court-container" style={{ position: "relative" }}>
|
||||||
|
<CourtSvg id="court-svg" />
|
||||||
|
{players.map((player) => {
|
||||||
|
return (
|
||||||
|
<CourtPlayer
|
||||||
|
key={player.id}
|
||||||
|
player={player}
|
||||||
|
onRemove={() => onPlayerRemove(player)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
import { useRef } from "react"
|
||||||
|
import "../../style/player.css"
|
||||||
|
import RemoveIcon from "../../assets/icon/remove.svg?react"
|
||||||
|
import Draggable from "react-draggable"
|
||||||
|
import { PlayerPiece } from "./PlayerPiece"
|
||||||
|
import { Player } from "../../data/Player"
|
||||||
|
|
||||||
|
export interface PlayerProps {
|
||||||
|
player: Player
|
||||||
|
onRemove: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A player that is placed on the court, which can be selected, and moved in the associated bounds
|
||||||
|
* */
|
||||||
|
export default function CourtPlayer({ player, onRemove }: PlayerProps) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const x = player.rightRatio
|
||||||
|
const y = player.bottomRatio
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable handle={".player-piece"} nodeRef={ref} bounds="parent">
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={"player"}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: `${x * 100}%`,
|
||||||
|
top: `${y * 100}%`,
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
className="player-content"
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
if (e.key == "Delete") onRemove()
|
||||||
|
}}>
|
||||||
|
<div className="player-selection-tab">
|
||||||
|
<RemoveIcon
|
||||||
|
className="player-selection-tab-remove"
|
||||||
|
onClick={onRemove}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PlayerPiece team={player.team} text={player.role} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import React from "react"
|
||||||
|
import "../../style/player.css"
|
||||||
|
import { Team } from "../../data/Team"
|
||||||
|
|
||||||
|
export function PlayerPiece({ team, text }: { team: Team; text: string }) {
|
||||||
|
return (
|
||||||
|
<div className={`player-piece ${team}`}>
|
||||||
|
<p>{text}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Team } from "./Team"
|
||||||
|
|
||||||
|
export interface Player {
|
||||||
|
/**
|
||||||
|
* unique identifier of the player.
|
||||||
|
* This identifier must be unique to the associated court.
|
||||||
|
*/
|
||||||
|
id: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the player's team
|
||||||
|
* */
|
||||||
|
team: Team
|
||||||
|
|
||||||
|
/**
|
||||||
|
* player's position
|
||||||
|
* */
|
||||||
|
role: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
|
||||||
|
*/
|
||||||
|
bottomRatio: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
|
||||||
|
*/
|
||||||
|
rightRatio: number
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
export enum Team {
|
||||||
|
Allies = "allies",
|
||||||
|
Opponents = "opponents",
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
#court-container {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
background-color: var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#court-svg {
|
||||||
|
margin: 5%;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#court-svg * {
|
||||||
|
stroke: var(--selected-team-secondarycolor);
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--main-color: #ffffff;
|
--main-color: #ffffff;
|
||||||
--second-color: #ccde54;
|
--second-color: #ccde54;
|
||||||
|
|
||||||
--background-color: #d2cdd3;
|
--background-color: #d2cdd3;
|
||||||
}
|
|
||||||
|
--selected-team-primarycolor: #ffffff;
|
||||||
|
--selected-team-secondarycolor: #000000;
|
||||||
|
|
||||||
|
--selection-color: #3f7fc4;
|
||||||
|
}
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
as the .player div content is translated,
|
||||||
|
the real .player div position is not were the user can expect.
|
||||||
|
Disable pointer events to this div as it may overlap on other components
|
||||||
|
on the court.
|
||||||
|
*/
|
||||||
|
.player {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-piece {
|
||||||
|
font-family: monospace;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
background-color: var(--selected-team-primarycolor);
|
||||||
|
color: var(--selected-team-secondarycolor);
|
||||||
|
|
||||||
|
border-width: 2px;
|
||||||
|
border-radius: 100px;
|
||||||
|
border-style: solid;
|
||||||
|
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-selection-tab {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
margin-bottom: 10%;
|
||||||
|
justify-content: center;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-selection-tab-remove {
|
||||||
|
pointer-events: all;
|
||||||
|
height: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-selection-tab-remove * {
|
||||||
|
stroke: red;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-selection-tab-remove:hover * {
|
||||||
|
fill: #f1dbdb;
|
||||||
|
stroke: #ff331a;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player:focus-within .player-selection-tab {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player:focus-within .player-piece {
|
||||||
|
color: var(--selection-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player:focus-within {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
#main {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbar {
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--main-color);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#court-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#court {
|
||||||
|
max-width: 80%;
|
||||||
|
max-height: 80%;
|
||||||
|
}
|
@ -1,19 +1,13 @@
|
|||||||
|
|
||||||
interface DisplayResultsProps {
|
interface DisplayResultsProps {
|
||||||
results: readonly { name: string, description: string}[]
|
results: readonly { name: string; description: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DisplayResults({results}: DisplayResultsProps) {
|
export default function DisplayResults({ results }: DisplayResultsProps) {
|
||||||
const list = results
|
const list = results.map(({ name, description }) => (
|
||||||
.map(({name, description}) =>
|
|
||||||
<div>
|
<div>
|
||||||
<p>username: {name}</p>
|
<p>username: {name}</p>
|
||||||
<p>description: {description}</p>
|
<p>description: {description}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
))
|
||||||
return (
|
return <div>{list}</div>
|
||||||
<div>
|
|
||||||
{list}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import React, { CSSProperties, useState } from "react"
|
||||||
|
import "../style/visualizer.css"
|
||||||
|
import Court from "../assets/basketball_court.svg"
|
||||||
|
|
||||||
|
export default function Visualizer({ id, name }: { id: number; name: string }) {
|
||||||
|
const [style, setStyle] = useState<CSSProperties>({})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="main">
|
||||||
|
<div id="topbar">
|
||||||
|
<h1>{name}</h1>
|
||||||
|
</div>
|
||||||
|
<div id="court-container">
|
||||||
|
<img
|
||||||
|
id="court"
|
||||||
|
src={Court}
|
||||||
|
style={style}
|
||||||
|
alt="Basketball Court"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
parameters:
|
||||||
|
phpVersion: 70400
|
||||||
|
level: 6
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- public
|
||||||
|
scanFiles:
|
||||||
|
- config.php
|
||||||
|
- sql/database.php
|
||||||
|
- profiles/dev-config-profile.php
|
||||||
|
- profiles/prod-config-profile.php
|
||||||
|
excludePaths:
|
||||||
|
- src/react-display-file.php
|
@ -0,0 +1 @@
|
|||||||
|
../front
|
@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Gateway\AuthGateway;
|
||||||
|
use App\Http\HttpRequest;
|
||||||
|
use App\Http\HttpResponse;
|
||||||
|
use App\Http\ViewHttpResponse;
|
||||||
|
use App\Model\AuthModel;
|
||||||
|
use App\Validation\FieldValidationFail;
|
||||||
|
use App\Validation\ValidationFail;
|
||||||
|
use App\Validation\Validators;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
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", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $viewName
|
||||||
|
* @param ValidationFail[] $fails
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
private function displayBadFields(string $viewName, array $fails): HttpResponse {
|
||||||
|
$bad_fields = [];
|
||||||
|
foreach ($fails as $err) {
|
||||||
|
if ($err instanceof FieldValidationFail) {
|
||||||
|
$bad_fields[] = $err->getFieldName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ViewHttpResponse::twig($viewName, ['bad_fields' => $bad_fields]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed[] $request
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
public function confirmRegister(array $request): HttpResponse {
|
||||||
|
$fails = [];
|
||||||
|
$request = HttpRequest::from($request, $fails, [
|
||||||
|
"username" => [Validators::name(), Validators::lenBetween(2, 32)],
|
||||||
|
"password" => [Validators::lenBetween(6, 256)],
|
||||||
|
"confirmpassword" => [Validators::lenBetween(6, 256)],
|
||||||
|
"email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)],
|
||||||
|
]);
|
||||||
|
if (!empty($fails)) {
|
||||||
|
return $this->displayBadFields("display_register.html.twig", $fails);
|
||||||
|
}
|
||||||
|
$fails = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email']);
|
||||||
|
if (empty($fails)) {
|
||||||
|
$results = $this->model->getUserFields($request['email']);
|
||||||
|
return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]);
|
||||||
|
}
|
||||||
|
return $this->displayBadFields("display_register.html.twig", $fails);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function displayLogin(): HttpResponse {
|
||||||
|
return ViewHttpResponse::twig("display_login.html.twig", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed[] $request
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
public function confirmLogin(array $request): HttpResponse {
|
||||||
|
$fails = [];
|
||||||
|
$request = HttpRequest::from($request, $fails, [
|
||||||
|
"password" => [Validators::lenBetween(6, 256)],
|
||||||
|
"email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)],
|
||||||
|
]);
|
||||||
|
if (!empty($fails)) {
|
||||||
|
return $this->displayBadFields("display_login.html.twig", $fails);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fails = $this->model->login($request['email'], $request['password']);
|
||||||
|
if (empty($fails)) {
|
||||||
|
$results = $this->model->getUserFields($request['email']);
|
||||||
|
return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]);
|
||||||
|
}
|
||||||
|
return $this->displayBadFields("display_login.html.twig", $fails);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
require_once __DIR__ . "/../react-display.php";
|
||||||
|
|
||||||
|
use App\Gateway\FormResultGateway;
|
||||||
|
use App\Http\HttpRequest;
|
||||||
|
use App\Http\HttpResponse;
|
||||||
|
use App\Http\ViewHttpResponse;
|
||||||
|
use App\Validation\Validators;
|
||||||
|
|
||||||
|
class SampleFormController {
|
||||||
|
private FormResultGateway $gateway;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param FormResultGateway $gateway
|
||||||
|
*/
|
||||||
|
public function __construct(FormResultGateway $gateway) {
|
||||||
|
$this->gateway = $gateway;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function displayFormReact(): HttpResponse {
|
||||||
|
return ViewHttpResponse::react("views/SampleForm.tsx", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function displayFormTwig(): HttpResponse {
|
||||||
|
return ViewHttpResponse::twig('sample_form.html.twig', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $form
|
||||||
|
* @param callable(array<array<string, string>>): ViewHttpResponse $response
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
private function submitForm(array $form, callable $response): HttpResponse {
|
||||||
|
return Control::runCheckedFrom($form, [
|
||||||
|
"name" => [Validators::lenBetween(0, 32), Validators::name("Le nom ne peut contenir que des lettres, des chiffres et des accents")],
|
||||||
|
"description" => [Validators::lenBetween(0, 512)],
|
||||||
|
], function (HttpRequest $req) use ($response) {
|
||||||
|
$description = htmlspecialchars($req["description"]);
|
||||||
|
$this->gateway->insert($req["name"], $description);
|
||||||
|
$results = ["results" => $this->gateway->listResults()];
|
||||||
|
return call_user_func_array($response, [$results]);
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $form
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
public function submitFormTwig(array $form): HttpResponse {
|
||||||
|
return $this->submitForm($form, fn(array $results) => ViewHttpResponse::twig('display_results.html.twig', $results));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $form
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
public function submitFormReact(array $form): HttpResponse {
|
||||||
|
return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Http\HttpCodes;
|
||||||
|
use App\Http\HttpResponse;
|
||||||
|
use App\Http\JsonHttpResponse;
|
||||||
|
use App\Http\ViewHttpResponse;
|
||||||
|
use App\Model\TacticModel;
|
||||||
|
|
||||||
|
class VisualizerController {
|
||||||
|
private TacticModel $tacticModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TacticModel $tacticModel
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function __construct(TacticModel $tacticModel) {
|
||||||
|
$this->tacticModel = $tacticModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openVisualizer(int $id): HttpResponse {
|
||||||
|
$tactic = $this->tacticModel->get($id);
|
||||||
|
|
||||||
|
if ($tactic == null) {
|
||||||
|
return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Gateway;
|
||||||
|
|
||||||
|
use App\Connexion;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class AuthGateway {
|
||||||
|
private Connexion $con;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Connexion $con
|
||||||
|
*/
|
||||||
|
public function __construct(Connexion $con) {
|
||||||
|
$this->con = $con;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function mailExist(string $email): bool {
|
||||||
|
return $this->getUserFields($email) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function insertAccount(string $username, string $hash, string $email): void {
|
||||||
|
$this->con->exec("INSERT INTO AccountUser VALUES (:username,:hash,:email)", [':username' => [$username, PDO::PARAM_STR],':hash' => [$hash, PDO::PARAM_STR],':email' => [$email, PDO::PARAM_STR]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserHash(string $email): string {
|
||||||
|
$results = $this->con->fetch("SELECT hash FROM AccountUser WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]);
|
||||||
|
return $results[0]['hash'];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $email
|
||||||
|
* @return array<string,string>|null
|
||||||
|
*/
|
||||||
|
public function getUserFields(string $email): ?array {
|
||||||
|
$results = $this->con->fetch("SELECT username,email FROM AccountUser WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]);
|
||||||
|
$firstRow = $results[0] ?? null;
|
||||||
|
return $firstRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Gateway;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use App\Connexion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sample gateway, that stores the sample form's result.
|
||||||
|
*/
|
||||||
|
class FormResultGateway {
|
||||||
|
private Connexion $con;
|
||||||
|
|
||||||
|
public function __construct(Connexion $con) {
|
||||||
|
$this->con = $con;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function insert(string $username, string $description): void {
|
||||||
|
$this->con->exec(
|
||||||
|
"INSERT INTO FormEntries VALUES (:name, :description)",
|
||||||
|
[
|
||||||
|
":name" => [$username, PDO::PARAM_STR],
|
||||||
|
"description" => [$description, PDO::PARAM_STR],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function listResults(): array {
|
||||||
|
return $this->con->fetch("SELECT * FROM FormEntries", []);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Model;
|
||||||
|
|
||||||
|
use App\Controller\AuthController;
|
||||||
|
use App\Gateway\AuthGateway;
|
||||||
|
use App\Validation\FieldValidationFail;
|
||||||
|
use App\Validation\ValidationFail;
|
||||||
|
|
||||||
|
class AuthModel {
|
||||||
|
private AuthGateway $gateway;
|
||||||
|
/**
|
||||||
|
* @param AuthGateway $gateway
|
||||||
|
*/
|
||||||
|
public function __construct(AuthGateway $gateway) {
|
||||||
|
$this->gateway = $gateway;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $username
|
||||||
|
* @param string $password
|
||||||
|
* @param string $confirmPassword
|
||||||
|
* @param string $email
|
||||||
|
* @return ValidationFail[]
|
||||||
|
*/
|
||||||
|
public function register(string $username, string $password, string $confirmPassword, string $email): array {
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ($password != $confirmPassword) {
|
||||||
|
$errors[] = new FieldValidationFail("confirmpassword", "password and password confirmation are not equals");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->gateway->mailExist($email)) {
|
||||||
|
$errors[] = new FieldValidationFail("email", "email already exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(empty($errors)) {
|
||||||
|
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
$this->gateway->insertAccount($username, $hash, $email);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $email
|
||||||
|
* @return array<string,string>|null
|
||||||
|
*/
|
||||||
|
public function getUserFields(string $email): ?array {
|
||||||
|
return $this->gateway->getUserFields($email);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $email
|
||||||
|
* @param string $password
|
||||||
|
* @return ValidationFail[] $errors
|
||||||
|
*/
|
||||||
|
public function login(string $email, string $password): array {
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if (!$this->gateway->mailExist($email)) {
|
||||||
|
$errors[] = new FieldValidationFail("email", "email doesnt exists");
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
$hash = $this->gateway->getUserHash($email);
|
||||||
|
|
||||||
|
if (!password_verify($password, $hash)) {
|
||||||
|
$errors[] = new FieldValidationFail("password", "invalid password");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Profil Utilisateur</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
background-color: #7FBFFF;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="user-profile">
|
||||||
|
<h1>Votre profil</h1>
|
||||||
|
<p><strong>Pseudo : </strong> {{ username }} </p>
|
||||||
|
<p><strong>Email : {{ email }} </strong></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,85 @@
|
|||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Connexion</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"], input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"] {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"]:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
{% for err in bad_fields %}
|
||||||
|
.form-group #{{ err }} {
|
||||||
|
border-color: red;
|
||||||
|
}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<center><h2>Se connecter</h2></center>
|
||||||
|
<form action="login" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email :</label>
|
||||||
|
<input type="text" id="email" name="email" required>
|
||||||
|
<label for= "password">Mot de passe :</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" value="S'identifier">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,88 @@
|
|||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>S'enregistrer</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"], input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"] {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"]:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
{% for err in bad_fields %}
|
||||||
|
.form-group #{{ err }} {
|
||||||
|
border-color: red;
|
||||||
|
}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<center><h2>S'enregistrer</h2></center>
|
||||||
|
<form action="register" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Nom d'utilisateur :</label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
<label for= "password">Mot de passe :</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
<label for="confirmpassword">Confirmer le mot de passe :</label>
|
||||||
|
<input type="password" id="confirmpassword" name="confirmpassword" required>
|
||||||
|
|
||||||
|
<label for="email">Email :</label>
|
||||||
|
<input type="text" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" value="Confirmer">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,19 @@
|
|||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Twig view</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>Hello, this is a sample form made in Twig !</h1>
|
||||||
|
|
||||||
|
<form action="submit-twig" method="POST">
|
||||||
|
<label for="name">your name: </label>
|
||||||
|
<input type="text" id="name" name="name"/>
|
||||||
|
<label for="password">a little description about yourself: </label>
|
||||||
|
<input type="text" id="password" name="description"/>
|
||||||
|
<input type="submit" value="click me to submit!"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
## verify php and typescript types
|
||||||
|
|
||||||
|
echo "running php typechecking"
|
||||||
|
vendor/bin/phpstan analyze && echo "php types are respected"
|
||||||
|
|
||||||
|
echo "running typescript typechecking"
|
||||||
|
npm run tsc && echo "typescript types are respected"
|
Loading…
Reference in new issue