diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..334dc3f --- /dev/null +++ b/config/database.php @@ -0,0 +1,9 @@ + 'localhost', + 'port' => 3306, + 'user' => 'benjamin', + 'pass' => '011235813', + 'name' => 'siterecette' +]; \ No newline at end of file diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 9e6b0eb..898ca90 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -1,3 +1,4 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap'); /* Sommaire : - body et html - Header et ses contenues @@ -14,6 +15,7 @@ html, body{ height: 100%; text-align: center; /*font-size: 98%;*/ + font-family: "Roboto", sans-serif; } @@ -43,6 +45,11 @@ html, body{ box-shadow: 1px 1px 1px black; } +.logo:hover{ + box-shadow: 1px 1px 2px #0bbd90; + border: 1px solid #0bbd90; +} + nav { flex: 9; text-align: initial; @@ -64,6 +71,25 @@ nav { .nav-element { line-height: 2.5; padding: 10px; + text-decoration: none; + color: black; +} + +.nav-list{ + li:nth-of-type(3){ + position: fixed; + right: 25px; + } +} + +#login { + border: 1px solid black; + border-radius: 6px; + background-color: #ffc478; +} + +#login:hover { + background-color: #0bbd90; } /*Body et son contenu */ @@ -77,21 +103,93 @@ body { } .sidebar { - background-color: aquamarine; + background-color: #caecd6; border: 1px solid rgba(0,0,0,.125); - border-radius: 10px; + border-radius: 6px; width: 128px; + display: flex; + flex-direction: column; + overflow: scroll; } +.tag-cont { + /*display:none;*/ +} + +.ingr-cont { + /*display:none;*/ +} + +.tag-selected-div { + flex: 4; +} + +.tag-unselected-div { + flex: 4; +} + +.sidebar-search { + flex: 1; +} + +.search-form-tag { + width: 85%; +} + +.search-form-recette { + width: 100%; + height: 25px; +} + +.search-form { + border: 1px solid black; + border-radius: 20px; + box-shadow: 0px 0px 10px rgb(131, 131, 131); +} + +nav { + display: flex; + flex-direction: column; + justify-content: space-around; +} + +.tag { + border: 1px solid black; + border-radius: 10px; + width: 95%; + box-shadow: 1px 1px 1px black; +} + +.tag-selected { + background-color: #ffa04d; +} + +.tag-selected:hover { + background-color: white; +} + +.tag-unselected { + background-color: white; + +} +.tag-unselected:hover { + background-color: #ffa04d; +} + +#hr { + border: 1px solid #0bbd90; + width: 93%; + box-shadow: 1px 3px 5px #0bbd90; +} .main-body { display: flex; flex-direction: row; - overflow: auto; flex: 1; background-clip: border-box; - padding-right: 5%; - padding-left: 5%; + padding-right: 2%; + padding-left: 2%; + overflow: hidden; } /* content : le cadre de base du centre de notre site @@ -103,37 +201,77 @@ body { height: 100%; border: 1px solid rgba(0,0,0,.125); border-radius: 6px; - overflow: auto; + overflow: scroll; } /*Recettes : représente le contenue centrale de la page. */ .recettes { - --UneVariable: calc( 100vw / 500 ); display: grid; - grid-template-columns: repeat(var(--UneVariable), 1fr); + grid-template-columns: repeat(3, 1fr); grid-gap: 10px; - grid-auto-rows: minmax(300px, auto); - grid-auto-columns: auto; + /* grid-auto-rows: minmax(300px, auto); + grid-auto-columns: auto;*/ + padding: 10px; +} + +@media screen and (max-width : 1200px) { + .recettes { + grid-template-columns: repeat(2, 1fr); + } +} + +@media screen and (max-width : 800px) { + .recettes { + grid-template-columns: repeat(1, 1fr); + } + +} + +@media screen and (max-width : 600px) { + + .sidebar { + display: none; + } +} + +ul { + list-style-type: none; + margin-left: 0px; + text-align: center; + padding : 0px; } /* Classe recette icone : C'est la classe de chaques éléments de notre liste de recette */ .recette-icone{ + display: flex; border: 3px solid rgba(0,0,0,.125); border-radius: 40px; text-align: left; padding: 15px; vertical-align: middle; box-shadow: 3px 5px 5px black; - width: 500px; + max-width: 400px; + text-decoration: none; + color: black; +} + +.recette-icone:hover{ + border-color: #0bbd90; +} + +.recette-icone-content{ + display: flex; + flex-direction: column; + padding-left: 10px; } .recette-preview-image{ - max-width: calc( 10vh + 10vw ); - max-height: calc( 10vh + 10vw ); + max-width: calc( 5vh + 5vw ); + max-height: calc( 5vh + 5vw ); border: 1px solid rgb(252, 223, 57); border-radius: 25px; } @@ -165,11 +303,102 @@ body { justify-content: center; } -/*description*/ -.recette-desc { - padding : 10px; +.recette-div-info { + display: flex; + flex-direction: row; } +.recette-div-liste-info{ + padding: 50px; + margin: 10px; +} + +.recette-liste-info-elem { + text-align: left; +} + +.recette-liste-info { + display: flex; + flex-direction: column; +} + +.recette-liste-tag { + display: flex; + flex-direction: row; +} + + +/*description*/ +.recette-desc { + padding : 30px; +} + +/*Login form*/ +.formcont { + flex: 1; + display: flex; + justify-content: center; + align-items: center; +} + +.formcont form { + width: 50vmin; + height: 40vmin; + border: 2px solid rgb(0, 0, 0); + padding: 20px; + display: flex; + flex-direction: column; + gap: 1.5vmin; + background:white; + border-radius: 12px; + box-shadow: 5px 5px 30px #555; +} + +.formcont form .form-group { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + padding: 1.2vmin; + padding-left:0px; +} + +.formcont form .form-group label { + width: 95%; + text-align: center; + color:black; + font-size:5vmin; +} + +.formcont form .form-group input { + width: 95%; + box-sizing: border-box; + font-size:3vmin; + padding:1vmin; + +} + +.formcont form button { + width: 48%; + height: 19%; + padding:0.5vmin; + box-sizing: border-box; + margin: 0 auto; + background: #ffa04d; + color: black; + font-size: 5vmin; + border-radius: 7px; + box-shadow: 2px 2px 0px #000000; +} + +.btn:hover { + background-color: #0bbd90; +} + +.form-control { + border-radius: 10px; + box-shadow: 3px 4px 5px #8c8c8c; +} /*Footer et son contenue*/ footer{ diff --git a/public/assets/js/login.js b/public/assets/js/login.js new file mode 100644 index 0000000..e14ca46 --- /dev/null +++ b/public/assets/js/login.js @@ -0,0 +1,66 @@ +document.addEventListener( 'DOMContentLoaded', function(){ + const FORM = document.getElementById( 'login-form' ); + const FORM_ERROR = document.getElementById( 'login-errors' ); + if( !FORM ) + return; + + /** + * Permet d'afficher une erreur dans le formulaire. + * @param message + */ + FORM.showError = function( message ){ + FORM_ERROR.style.display = 'block'; + FORM_ERROR.innerText = message; + } + + /** + * Permet de retirer l'erreur du formulaire. + */ + FORM.removeError = function(){ + FORM_ERROR.style.display = 'none'; + FORM_ERROR.innerText = ''; + } + + /** + * Gère l'envoi du formulaire. + * @param e + * @returns {boolean} + */ + FORM.onsubmit = function( e ){ + + e.preventDefault(); + FORM.removeError(); + + let userField = e.target.username; + let passwordField = e.target.password; + + if( !userField || !passwordField || userField.value === "" || passwordField.value === "" ){ + FORM.showError( "Le nom d'utilisateur ou le mot de passe ne doivent pas être vide."); + return false; + } + + let formData = new FormData( FORM ); + let options = { + method: "POST", + contentType: "application/json", + body: formData, + } + + fetch( FORM.action, options ).then( ( response ) => { + if( response.ok ){ + response.json().then( ( responseJSON ) => { + + if( responseJSON.success === true ){ + window.location.href = window.location.origin; // Redirection sur la page d'accueil si succès. + } else { + FORM.showError( responseJSON.message || "Mauvais nom d'utilisateur ou mauvais mot de passe." ); + } + + }) + } + }) + + } + + +}) \ No newline at end of file diff --git a/src/Domain/Ingredients/Ingredient.php b/src/Domain/Ingredients/Ingredient.php new file mode 100644 index 0000000..bfab46f --- /dev/null +++ b/src/Domain/Ingredients/Ingredient.php @@ -0,0 +1,13 @@ + 'Ingredient', + 'columns' => [ + 'num_ingredient', 'nom_ingredient' + ] + ]; + + } + + public function add( Model $ingredient ): bool { + + } +} \ No newline at end of file diff --git a/src/Domain/Model.php b/src/Domain/Model.php new file mode 100644 index 0000000..7c7d6c6 --- /dev/null +++ b/src/Domain/Model.php @@ -0,0 +1,8 @@ +description_recette ); + } + +} \ No newline at end of file diff --git a/src/Domain/Recettes/RecetteRepository.php b/src/Domain/Recettes/RecetteRepository.php new file mode 100644 index 0000000..53bc4a2 --- /dev/null +++ b/src/Domain/Recettes/RecetteRepository.php @@ -0,0 +1,66 @@ + 'Recette', + 'columns' => [ + 'num_recette', 'titre_recette', 'slug', 'description_recette', 'photo', 'publication_date', 'temps_de_preparation' + ] + ]; + + } + + /** + * Permet d'avoir une recette par un ID. + * + * @param int $id + * @return Recette|null + */ + public function getByID( int $id ): ?Recette { + $sqlQuery = "SELECT * FROM {$this->tableName} WHERE num_recette = {$id}"; + $results = $this->selectGetAll($sqlQuery); + if( $results === null || count( $results ) > 1 ) + return null; + return $results[0]; + } + + /** + * Permet d'avoir une recette par un slug. + * + * @param string $slug + * @return Recette|null + */ + public function getBySlug( string $slug ): ?Recette { + $sqlQuery = "SELECT * FROM {$this->tableName} WHERE slug = {$slug}"; + $results = $this->selectGetAll($sqlQuery); + if( $results === null || count( $results ) > 1 ) + return null; + return $results[0]; + } + + public function add( Model $recette ): bool { + return $this->addEntity( $recette ); + } + + public function update( Model $recette ): bool { + return $this->updateEntity( $recette, 'num_recette' ); + } + + public function delete( Model $recette ): bool { + return $this->deleteEntity( $recette, 'num_recette' ); + } + +} \ No newline at end of file diff --git a/src/Domain/Repository.php b/src/Domain/Repository.php new file mode 100644 index 0000000..b1933c3 --- /dev/null +++ b/src/Domain/Repository.php @@ -0,0 +1,99 @@ + '', 'columns' => [ ...Liste des colonnes dans la BDD ] ]. + * @return array + */ + abstract public static function getStructure(): array; + + final public string $tableName; + final public array $tableColumns; + + public function __construct(){ + $structure = static::getStructure(); + + $this->tableName = $structure['table']; + $this->tableColumns = $structure['columns']; + } + + + /** + * Permet d'avoir tous les éléments correspondant à la requête passée en paramètre. + * + * @param string $sqlQuery + * @return array|null + */ + public function selectGetAll( string $sqlQuery ): ?array { + $statement = Kernel::$DB->pdo->prepare( $sqlQuery ); + if( !$statement->execute() ) + return null; + + $results = $statement->fetchAll( PDO::FETCH_CLASS, static::getEntity() ); + if( empty( $results ) ) + return null; + return $results; + } + + public function addEntity( Model $entity ): bool { + + $query = "INSERT INTO {$this->tableName} ("; + $values = "("; + foreach( $this->tableColumns as $column ) { + $query .= "`{$column}`,"; + $values .= ":{$column},"; + } + $query = substr( $query, 0, -1 ) . ")"; + $values = substr( $values, 0, -1 ) . ")"; + $query .= " VALUES " . $values . ";"; + + $statement = Kernel::$DB->pdo->prepare( $query ); + foreach( $this->tableColumns as $column ) { + $statement->bindValue(":{$column}", $entity->{$column} ); + } + + return $statement->execute(); + } + + public function updateEntity( Model $entity, string $identifier ): bool { + $query = "UPDATE {$this->tableName} SET "; + foreach( $this->tableColumns as $column ) { + $query .= "`{$column}` = :{$column},"; + } + $query = substr( $query, 0, -1 ) . " WHERE {$identifier} = :{$identifier};"; + + $statement = Kernel::$DB->pdo->prepare( $query ); + foreach( $this->tableColumns as $column ) { + $statement->bindValue(":{$column}", $entity->{$column} ); + } + $statement->bindValue( ":{$identifier}", $entity->{$identifier} ); + return $statement->execute(); + } + + public function deleteEntity( Model $entity, string $identifier ): bool { + $query = "DELETE FROM {$this->tableName} WHERE {$identifier} = :{$identifier};"; + $statement = Kernel::$DB->pdo->prepare( $query ); + $statement->bindValue( ":{$identifier}", $entity->{$identifier} ); + return $statement->execute(); + } + + abstract public function add( Model $entity ): bool; + abstract public function update( Model $entity ): bool; + abstract public function delete( Model $entity ): bool; + + +} diff --git a/src/Domain/Utilisateurs/AuthentificationController.php b/src/Domain/Utilisateurs/AuthentificationController.php index 04c1d30..f1e93e8 100644 --- a/src/Domain/Utilisateurs/AuthentificationController.php +++ b/src/Domain/Utilisateurs/AuthentificationController.php @@ -6,6 +6,7 @@ use App\Domain\Controller; use App\Helpers\Authentification; use App\Http\JSONResponse; use App\Http\Request; +use App\Infrastructure\View; class AuthentificationController extends Controller { @@ -15,19 +16,29 @@ class AuthentificationController extends Controller { // Public routes. self::Route( routeUrl: '/login', routeName: 'login', routeAction: 'loginForm', pageHeadTitle: 'Connexion' ), + self::Route( routeUrl: '/logout', routeName: 'logout', routeAction: 'logoutPage', pageHeadTitle: 'Déconnexion' ), // API Routes. self::Route( routeUrl: '/api/auth', routeName: 'api->auth', routeAction: 'auth', routeMethods: ['POST'] ), - self::Route( routeUrl: '/api/auth/logout', routeName: 'api->auth->logout', routeAction: 'logout', routeMethods: ['POST'] ), + // self::Route( routeUrl: '/api/auth/logout', routeName: 'api->auth->logout', routeAction: 'logout', routeMethods: ['POST'] ), ]; } - public function login(): View { + public function loginForm(): View { return new View( 'login' ); } + public function logoutPage(){ + if( !Authentification::isLoggedIn() ) { + Request::redirectTo( 'home' ); + } + + Authentification::destroySession(); + Request::redirectTo( 'home' ); + } + public function auth(): JSONResponse { Request::setCORS(); @@ -39,14 +50,14 @@ class AuthentificationController extends Controller { $userId = 1; Authentification::loginUser( $userId ); - JSONResponse::sendSuccess( [ 'user_id' => $userId ] ); + return JSONResponse::sendSuccess( [ 'user_id' => $userId ] ); } public function logout(): JSONResponse { if( !Authentification::isLoggedIn() ) { - return JSONResponse::sendError( [ 'message' => 'Alrady disconnected' ] ); + return JSONResponse::sendError( [ 'message' => 'Already disconnected' ] ); } Authentification::destroySession(); diff --git a/src/Helpers/Markdown.php b/src/Helpers/Markdown.php new file mode 100644 index 0000000..bfe4b55 --- /dev/null +++ b/src/Helpers/Markdown.php @@ -0,0 +1,29 @@ + '$1', + '/\*(.*?)\*/' => '$1', + + // Titres + '/^## (.*?)$/m' => '

$1

', + '/^# (.*?)$/m' => '

$1

', + + ]; + } + + public static function convertToHTML( string $markdown ): string { + $safeMD = htmlspecialchars( $markdown, ENT_QUOTES ); + foreach( Markdown::getMarkdownEntities() as $key => $value ) { + $safeMD = preg_replace( $key, $value, $safeMD ); + } + + return nl2br( $safeMD ); + } +} \ No newline at end of file diff --git a/src/Http/Request.php b/src/Http/Request.php index 7172602..a4c49df 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -50,4 +50,20 @@ class Request { return self::sanitize( $_POST[$name] ); } + /** + * Permet de rediriger un utilisateur. + * /!\ Arrête tout traitement sur la page. + * + * @param string $routeName + * @param ...$args + * + * @return never + */ + public static function redirectTo( string $routeName, ...$args ): never { + + $routeUrl = Router::getRouteURL( $routeName, ...$args ); + header("Location: {$routeUrl}"); + die(); + } + } \ No newline at end of file diff --git a/src/Infrastructure/Database.php b/src/Infrastructure/Database.php new file mode 100644 index 0000000..7209ad4 --- /dev/null +++ b/src/Infrastructure/Database.php @@ -0,0 +1,45 @@ +databaseInfos = $loginInfos; + $this->tryLogin(); + } + + /** + * Permet d'essayer un login à la base de données. + * @return void - never si la connexion échoue. + */ + private function tryLogin(): void { + try { + $dsn = 'mysql:dbname=' . $this->databaseInfos['name'] . ';host=' . $this->databaseInfos['host'] . ';port=' . $this->databaseInfos['port']; + $this->pdo = new \PDO( $dsn, $this->databaseInfos['user'], $this->databaseInfos['pass'] ); + } catch ( \PDOException $e ) { + die( $e->getMessage() ); + } + } + +} \ No newline at end of file diff --git a/src/Kernel.php b/src/Kernel.php index 83017c6..485eafd 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -7,6 +7,7 @@ use App\Helpers\Authentification; use App\Helpers\AutoLoader; use App\Helpers\ConfigFactory; use App\Http\Router; +use App\Infrastructure\Database; /** * Classe primaire du site. @@ -22,6 +23,14 @@ final class Kernel { */ public private(set) static array $configs = []; + /** + * Instance à la base de données. + * Pour utiliser la classe PDO : $DB->PDO. + * + * @var Database|null + */ + public private(set) static ?Database $DB = null; + /** * Instance actuelle de l'application. * @var Kernel|null @@ -62,6 +71,7 @@ final class Kernel { public function init(): void { $this->buildAutoloader(); $this->loadConfig(); + $this->loadDatabase(); Authentification::startSession(); @@ -89,6 +99,7 @@ final class Kernel { try { self::$configs['general'] = ConfigFactory::loadConfigFile('general'); + self::$configs['database'] = ConfigFactory::loadConfigFile('database'); self::$configs['route_arguments'] = ConfigFactory::loadConfigFile('route_arguments'); self::$configs['views'] = ConfigFactory::loadConfigFile('views'); @@ -97,4 +108,12 @@ final class Kernel { } } + /** + * Permet de se connecter à la base de données principale. + * @return void + */ + private function loadDatabase(): void { + self::$DB = new Database( self::$configs['database'] ); + } + } \ No newline at end of file diff --git a/views/home.php b/views/home.php index 3cb93ff..c0c0f44 100644 --- a/views/home.php +++ b/views/home.php @@ -1,2 +1,9 @@

Coucou

- \ No newline at end of file +$md

"; ?> + +getByID( 4 ); +$rec->temps_de_preparation = 7; +$rec->titre_recette = "Tutu"; + +new \App\Domain\Recettes\RecetteRepository()->delete( $rec ); +?> diff --git a/views/login.php b/views/login.php new file mode 100644 index 0000000..7835617 --- /dev/null +++ b/views/login.php @@ -0,0 +1,23 @@ + + +
+
auth' ); ?>' method='post'> + +
+ +
+ +
+ +
+ +
+ +
+ + + +
+
+ diff --git a/views/partials/header.php b/views/partials/header.php index 792aadb..2848cdd 100644 --- a/views/partials/header.php +++ b/views/partials/header.php @@ -8,16 +8,22 @@ -