first commit

This commit is contained in:
2025-11-02 18:08:53 +01:00
commit cb045fb9e1
23 changed files with 1363 additions and 0 deletions

72
.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="304bc2cf-dc17-4cf0-8c50-b91baa1bac4a" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ComposerSettings">
<execution />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="PHP File" />
</list>
</option>
</component>
<component name="ProjectColorInfo"><![CDATA[{
"customColor": "",
"associatedIndex": 3
}]]></component>
<component name="ProjectId" id="34vOas5nhI714FKr1LdQhjIFwqK" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.ShowReadmeOnStart": "true",
"SHELLCHECK.PATH": "C:\\Users\\elrao\\AppData\\Roaming\\JetBrains\\PhpStorm2025.2\\plugins\\Shell Script\\shellcheck.exe",
"last_opened_file_path": "C:/Users/elrao/OneDrive/Documents/Projet_CMS",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\elrao\OneDrive\Documents\Projet_CMS" />
</key>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-3aa1da707db6-JavaScript-PS-252.26830.95" />
<option value="bundled-php-predefined-a98d8de5180a-9b563d34894c-com.jetbrains.php.sharedIndexes-PS-252.26830.95" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="304bc2cf-dc17-4cf0-8c50-b91baa1bac4a" name="Changes" comment="" />
<created>1762090649427</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1762090649427</updated>
<workItem from="1762090650716" duration="11813000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

9
Dockerfile Normal file
View File

@@ -0,0 +1,9 @@
FROM php:8.2-apache
LABEL authors="abdr-cms"
COPY . /var/www/html
WORKDIR /var/www/html
RUN docker-php-ext-install pdo pdo_mysql
EXPOSE 80

48
README.md Normal file
View File

@@ -0,0 +1,48 @@
Pour accéder au site : Lancez les conteneurs depuis la racine du projet avec la commande suivante (Dans un terminal qui accepte le bash par exemple GIT bash) :
./launch-dockers.sh
Une fois les conteneurs démarrés, le site sera accessible dans votre navigateur à l'adresse :
http://localhost:8080/
Accès au compte admin :
Login : user
Mdp: usecms123
Arborescence :
Projet_CMS/
├── admin/
│ ├── add.php
│ ├── board.php
│ ├── delete.php
│ ├── login.php
│ ├── logout.php
│ └── modif.php
├── assets/
│ ├── add.css
│ ├── article.css
│ ├── board.css
│ ├── delete.css
│ ├── login.css
│ ├── modif.css
│ └── style.css
├── bdd/
│ ├── Dockerfile
│ └── init.sql
├── include/
│ ├── authenticator.php
│ └── bd.php
├── article.php
├── Dockerfile
├── index.php
├── launch-dockers.sh
└── README.md

59
admin/add.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
require '../include/db.php';
require '../include/authenticator.php';
requireLogin();
$errors = [];
$titre = '';
$contenu = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$titre = trim($_POST['titre'] ?? '');
$contenu = trim($_POST['contenu'] ?? '');
if ($titre === '' || $contenu === '') {
$errors[] = 'Tous les champs sont obligatoires.';
} else {
$stmt = $pdo->prepare('INSERT INTO articles (titre, contenu, date_creation) VALUES (:titre, :contenu, :date)');
$stmt->execute([
':titre' => $titre,
':contenu' => $contenu,
':date' => date('Y-m-d H:i:s'),
]);
header('Location:board.php');
exit;
}
}
?>
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Ajouter un article</title>
<link rel="stylesheet" href="../assets/add.css">
</head>
<body>
<header>
<h1>Ajouter un article</h1>
</header>
<main>
<?php foreach ($errors as $e): ?>
<p class="error"><?= htmlspecialchars($e) ?></p>
<?php endforeach; ?>
<form method="post">
<label>Titre
<input type="text" name="titre" value="<?= htmlspecialchars($titre) ?>" required>
</label>
<label>Contenu
<textarea name="contenu" rows="10" required><?= htmlspecialchars($contenu) ?></textarea>
</label>
<div style="display:flex; gap:10px; justify-content:center;">
<button type="submit">Publier</button>
<a href="board.php">Annuler</a>
</div>
</form>
</main>
</body>
</html>

42
admin/board.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
require '../include/db.php';
require '../include/authenticator.php';
requireLogin();
$stmt = $pdo->query('SELECT * FROM articles ORDER BY date_creation DESC');
$articles = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"><title>Admin - Tableau de bord</title></head>
<link rel="stylesheet" href="../assets/board.css">
<body>
<header>
<h1>Tableau de bord</h1>
<div class="header-buttons">
<a href="add.php" class="btn">Ajouter un article</a>
<a href="logout.php" class="btn">Se déconnecter</a>
</div>
</header>
<main>
<table>
<tr><th>ID</th><th>Titre</th><th>Actions</th></tr>
<?php foreach ($articles as $a): ?>
<tr>
<td><?= $a['id'] ?></td>
<td><?= htmlspecialchars($a['titre']) ?></td>
<td>
<a href="modif.php?id=<?= $a['id'] ?>" class="btn">Modifier</a>
<a href="delete.php?id=<?= $a['id'] ?>" class="btn">Supprimer</a>
</td>
</tr>
<?php endforeach; ?>
</table>
</main>
<footer>
<p>&copy; <?= date('Y') ?> CMS. Tous droits réservés par Abd'R.</p>
</footer>
</body>
</html>

54
admin/delete.php Normal file
View File

@@ -0,0 +1,54 @@
<?php
require '../include/db.php';
require '../include/authenticator.php';
requireLogin();
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
header('Location:board.php');
exit;
}
$stmt = $pdo->prepare('SELECT id, titre FROM articles WHERE id = :id');
$stmt->execute([':id' => $id]);
$article = $stmt->fetch();
if (!$article) {
header('Location: board.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['confirm']) && $_POST['confirm'] === 'yes') {
$d = $pdo->prepare('DELETE FROM articles WHERE id = :id');
$d->execute([':id' => $id]);
}
header('Location: board.php');
exit;
}
?>
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Supprimer l'article</title>
<link rel="stylesheet" href="../assets/delete.css">
</head>
<body>
<header>Supprimer l'article</header>
<main>
<p>Êtes-vous sûr de vouloir supprimer : <strong><?= htmlspecialchars($article['titre']) ?></strong> ?</p>
<form method="post">
<div class="form-buttons">
<button type="submit" name="confirm" value="yes">Oui, supprimer</button>
<a href="board.php" class="btn">Annuler</a>
</div>
</form>
</main>
</body>
</html>

38
admin/login.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
require '../include/db.php';
require '../include/authenticator.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (checkLogin($pdo, $_POST['login'], $_POST['password'])) {
header('Location: board.php');
exit;
} else {
$error = 'Identifiants incorrects';
}
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Connexion</title>
<link rel="stylesheet" href="../assets/login.css">
</head>
<body>
<nav>
<a href="../index.php">Accueil</a>
</nav>
<main>
<h1>Connexion</h1>
<form method="post">
<input type="text" name="login" placeholder="Login" required><br>
<input type="password" name="password" placeholder="Mot de passe" required><br>
<button type="submit">Se connecter</button>
</form>
</main>
<p class="error"><?= $error ?></p>
</body>
</html>

19
admin/logout.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
require '../include/authenticator.php';
$_SESSION = [];
if (!function_exists('logout_user')) {
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
session_destroy();
}
header('Location: ../index.php');
exit;
?>

73
admin/modif.php Normal file
View File

@@ -0,0 +1,73 @@
<?php
global $pdo;
require '../include/db.php';
require '../include/authenticator.php';
requireLogin();
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
header('Location: board.php');
exit;
}
$stmt = $pdo->prepare('SELECT * FROM articles WHERE id = :id');
$stmt->execute([':id' => $id]);
$article = $stmt->fetch();
if (!$article) {
header('Location: board.php');
exit;
}
$errors = [];
$titre = $article['titre'];
$contenu = $article['contenu'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$titre = trim($_POST['titre'] ?? '');
$contenu = trim($_POST['contenu'] ?? '');
if ($titre === '' || $contenu === '') {
$errors[] = 'Tous les champs sont obligatoires.';
} else {
$u = $pdo->prepare('UPDATE articles SET titre = :titre, contenu = :contenu WHERE id = :id');
$u->execute([':titre' => $titre, ':contenu' => $contenu, ':id' => $id]);
header('Location: board.php');
exit;
}
}
?>
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Modifier l'article</title>
<link rel="stylesheet" href="../assets/modif.css">
</head>
<body>
<header>Modifier l'article</header>
<section class="form-section">
<main>
<?php foreach ($errors as $e): ?>
<p class="error"><?= htmlspecialchars($e) ?></p>
<?php endforeach; ?>
<form method="post">
<label>Titre
<input type="text" name="titre" value="<?= htmlspecialchars($titre) ?>" required>
</label>
<label>Contenu
<textarea name="contenu" rows="10" required><?= htmlspecialchars($contenu) ?></textarea>
</label>
<div class="form-buttons">
<button type="submit">Enregistrer</button>
<a href="board.php" class="btn">Annuler</a>
</div>
</form>
</main>
</section>
</body>
</html>

35
article.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
require 'include/db.php';
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$stmt = $pdo->prepare('SELECT * FROM articles WHERE id = ?');
$stmt->execute([$id]);
$article = $stmt->fetch();
if (!$article) { http_response_code(404); die('<h1>404 - Article introuvable</h1>'); }
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title><?= htmlspecialchars($article['titre']) ?></title>
<link rel="stylesheet" href="../assets/article.css">
</head>
<body>
<header></header>
<main>
<h1 class="article-title"><?= htmlspecialchars($article['titre']) ?></h1>
<article>
<p><?= nl2br(htmlspecialchars($article['contenu'])) ?></p>
</article>
<a href="index.php" class="btn">Retour</a>
</main>
<footer>
<p>&copy; <?= date('Y') ?> CMS. Tous droits réservés par Abd'R.</p>
</footer>
</body>
</html>

127
assets/add.css Normal file
View File

@@ -0,0 +1,127 @@
/* --- Body --- */
body {
font-family: 'Segoe UI', Roboto, Arial, sans-serif;
background-color: #f3f0f9;
color: #333;
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* --- Bande violette en haut --- */
header {
background: linear-gradient(135deg, #6a11cb, #8e2de2);
color: #fff;
padding: 12px 0;
display: flex;
justify-content: center; /* titre centré */
align-items: center;
font-size: 1.3rem;
font-weight: 700;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
/* --- Espace distinct entre header et formulaire --- */
section.form-section {
margin-top: 40px; /* espace entre bande et formulaire */
flex: 1;
display: flex;
justify-content: center;
padding: 20px;
}
/* --- Formulaire stylé --- */
form {
display: flex;
flex-direction: column;
gap: 15px;
background: #fff;
padding: 30px 25px;
border-radius: 10px;
border: 1px solid #e0d4f7;
box-shadow: 0 3px 6px rgba(0,0,0,0.08);
width: 90%;
max-width: 600px;
}
/* --- Labels et Inputs --- */
form label {
display: flex;
flex-direction: column;
font-weight: 600;
color: #5b1dbb;
text-align: left;
}
form input[type="text"],
form textarea {
padding: 12px 15px;
border: 1px solid #dcd0f5;
border-radius: 6px;
font-size: 1rem;
resize: vertical;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
form input[type="text"]:focus,
form textarea:focus {
outline: none;
border-color: #8e2de2;
box-shadow: 0 0 5px rgba(142,45,226,0.5);
}
/* --- Boutons Publier et Annuler --- */
.form-buttons {
display: flex;
justify-content: center;
gap: 15px;
}
button,
a.btn {
padding: 12px 25px;
background: linear-gradient(135deg, #6a11cb, #8e2de2);
color: #fff;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: background 0.3s ease, transform 0.2s ease;
}
button:hover,
a.btn:hover {
background: linear-gradient(135deg, #8e2de2, #6a11cb);
transform: translateY(-2px);
}
/* --- Messages d'erreur --- */
.error {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
padding: 12px;
border-radius: 6px;
width: 90%;
max-width: 600px;
margin-bottom: 15px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
/* --- Footer violet --- */
footer {
background: #6a11cb;
color: #fff;
text-align: center;
padding: 15px 0;
width: 100%;
box-shadow: 0 -2px 5px rgba(0,0,0,0.2);
font-size: 0.95rem;
position: fixed;
bottom: 0;
}

81
assets/article.css Normal file
View File

@@ -0,0 +1,81 @@
/* --- Body --- */
body {
font-family: 'Segoe UI', Roboto, Arial, sans-serif;
background-color: #f3f0f9;
color: #333;
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* --- Bande violette en haut --- */
header {
background: linear-gradient(135deg, #6a11cb, #8e2de2);
height: 50px; /* hauteur réduite */
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
/* --- Main --- */
main {
flex: 1;
margin-top: 20px; /* espace sous header */
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
/* --- Titre de l'article --- */
.article-title {
color: #5b1dbb;
font-size: 2rem;
font-weight: 700;
margin-bottom: 20px;
text-align: center;
}
/* --- Article --- */
article {
background: #fff;
padding: 25px 20px;
border-radius: 10px;
border: 1px solid #e0d4f7;
box-shadow: 0 3px 6px rgba(0,0,0,0.08);
width: 90%;
max-width: 800px;
margin-bottom: 20px;
}
/* --- Bouton Retour --- */
a.btn {
padding: 12px 25px;
background: linear-gradient(135deg, #6a11cb, #8e2de2);
color: #fff;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: background 0.3s ease, transform 0.2s ease;
}
a.btn:hover {
background: linear-gradient(135deg, #8e2de2, #6a11cb);
transform: translateY(-2px);
}
/* --- Footer --- */
footer {
background: #6a11cb;
color: #fff;
text-align: center;
padding: 15px 0;
width: 100%;
font-size: 0.95rem;
box-shadow: 0 -2px 5px rgba(0,0,0,0.2);
position: fixed;
bottom: 0;
}

109
assets/board.css Normal file
View File

@@ -0,0 +1,109 @@
/* --- Body --- */
body {
font-family: 'Segoe UI', Roboto, Arial, sans-serif;
background-color: #f3f0f9;
color: #333;
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* --- Header violet --- */
header {
background: linear-gradient(135deg, #6a11cb, #8e2de2);
color: #fff;
padding: 30px 0 15px 0;
display: flex;
flex-direction: column;
align-items: center;
position: fixed;
top: 0;
left: 0;
width: 100%;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
z-index: 1000;
}
/* --- Titre Tableau de bord --- */
header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
}
/* --- Conteneur boutons --- */
.header-buttons {
margin-top: 10px;
display: flex;
gap: 10px;
}
/* --- Boutons violet --- */
.btn {
padding: 10px 20px;
background: linear-gradient(135deg, #6a11cb, #8e2de2);
color: #fff;
border: none;
border-radius: 6px;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: background 0.3s ease, transform 0.2s ease;
}
.btn:hover {
background: linear-gradient(135deg, #8e2de2, #6a11cb);
transform: translateY(-2px);
}
/* --- Main contenu --- */
main {
flex: 1;
margin-top: 140px; /* hauteur du header */
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
/* --- Tableau stylé --- */
table {
width: 90%;
max-width: 900px;
border-collapse: collapse;
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
margin-top: 20px;
}
table th, table td {
padding: 12px 15px;
border-bottom: 1px solid #ddd;
text-align: left;
}
table th {
background-color: #8e2de2;
color: #fff;
}
table tr:hover {
background-color: #f5e9ff;
}
/* --- Footer violet --- */
footer {
background: #6a11cb;
color: #fff;
text-align: center;
padding: 15px 0;
width: 100%;
box-shadow: 0 -2px 5px rgba(0,0,0,0.2);
font-size: 0.95rem;
position: fixed;
bottom: 0;
}

75
assets/delete.css Normal file
View File

@@ -0,0 +1,75 @@
/* --- Body --- */
body {
font-family: 'Segoe UI', Roboto, Arial, sans-serif;
background-color: #f3f0f9;
color: #333;
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* --- Bande violette en haut --- */
header {
background: linear-gradient(135deg, #6a11cb, #8e2de2);
color: #fff;
padding: 12px 0;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.3rem;
font-weight: 700;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
/* --- Section principale distincte --- */
section.form-section {
margin-top: 40px; /* espace sous header */
flex: 1;
display: flex;
justify-content: center;
padding: 20px;
}
/* --- Formulaire confirmation --- */
form {
display: flex;
flex-direction: column;
gap: 20px;
background: #fff;
padding: 25px 20px;
border-radius: 10px;
border: 1px solid #e0d4f7;
box-shadow: 0 3px 6px rgba(0,0,0,0.08);
width: 90%;
max-width: 500px;
text-align: center;
}
/* --- Boutons Supprimer / Annuler --- */
.form-buttons {
display: flex;
justify-content: center;
gap: 15px;
}
button,
a.btn {
padding: 12px 25px;
background: linear-gradient(135deg, #6a11cb, #8e2de2);
color: #fff;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: background 0.3s ease, transform 0.2s ease;
}
button:hover,
a.btn:hover {
background: linear-gradient(135deg, #8e2de2, #6a11cb);
transform: translateY(-2px);
}

119
assets/login.css Normal file
View File

@@ -0,0 +1,119 @@
/* --- Body --- */
body {
font-family: 'Segoe UI', Roboto, Arial, sans-serif;
background-color: #f3f0f9;
color: #333;
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center; /* CENTRAGE VERTICAL */
}
/* --- Header fixe violet --- */
nav {
background: linear-gradient(135deg, #6a11cb, #8e2de2);
color: #fff;
padding: 15px 30px;
display: flex;
justify-content: flex-start; /* Accueil à gauche */
align-items: center;
position: fixed;
top: 0;
left: 0;
width: 100%;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
z-index: 1000;
}
nav a {
color: #fff;
text-decoration: none;
font-weight: 600;
font-size: 1.1rem;
}
nav a:hover {
opacity: 0.8;
}
/* --- Conteneur du formulaire et du titre --- */
.login-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 80px; /* espace pour nav fixe */
}
/* --- Titre Connexion --- */
.login-container h1 {
color: #5b1dbb;
margin-bottom: 30px;
text-align: center;
}
/* --- Formulaire centré --- */
form {
display: flex;
flex-direction: column;
gap: 15px;
background: #fff;
padding: 40px 30px;
border-radius: 10px;
border: 1px solid #e0d4f7;
box-shadow: 0 3px 6px rgba(0,0,0,0.08);
width: 90%;
max-width: 400px;
text-align: center;
}
/* --- Inputs --- */
form input[type="text"],
form input[type="password"] {
padding: 12px 15px;
border: 1px solid #dcd0f5;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
form input[type="text"]:focus,
form input[type="password"]:focus {
outline: none;
border-color: #8e2de2;
box-shadow: 0 0 5px rgba(142,45,226,0.5);
}
/* --- Bouton Se connecter --- */
form button {
padding: 12px;
background: linear-gradient(135deg, #6a11cb, #8e2de2);
color: #fff;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.3s ease, transform 0.2s ease;
}
form button:hover {
background: linear-gradient(135deg, #8e2de2, #6a11cb);
transform: translateY(-2px);
}
/* --- Footer violet --- */
footer {
background: #6a11cb;
color: #fff;
text-align: center;
padding: 15px 0;
width: 100%;
box-shadow: 0 -2px 5px rgba(0,0,0,0.2);
font-size: 0.95rem;
position: fixed;
bottom: 0;
}

127
assets/modif.css Normal file
View File

@@ -0,0 +1,127 @@
/* --- Body --- */
body {
font-family: 'Segoe UI', Roboto, Arial, sans-serif;
background-color: #f3f0f9;
color: #333;
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* --- Bande violette en haut --- */
header {
background: linear-gradient(135deg, #6a11cb, #8e2de2);
color: #fff;
padding: 12px 0;
display: flex;
justify-content: center; /* titre centré */
align-items: center;
font-size: 1.3rem;
font-weight: 700;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
/* --- Espace distinct entre header et formulaire --- */
section.form-section {
margin-top: 40px; /* espace entre bande et formulaire */
flex: 1;
display: flex;
justify-content: center;
padding: 20px;
}
/* --- Formulaire stylé --- */
form {
display: flex;
flex-direction: column;
gap: 15px;
background: #fff;
padding: 30px 25px;
border-radius: 10px;
border: 1px solid #e0d4f7;
box-shadow: 0 3px 6px rgba(0,0,0,0.08);
width: 90%;
max-width: 600px;
}
/* --- Labels et Inputs --- */
form label {
display: flex;
flex-direction: column;
font-weight: 600;
color: #5b1dbb;
text-align: left;
}
form input[type="text"],
form textarea {
padding: 12px 15px;
border: 1px solid #dcd0f5;
border-radius: 6px;
font-size: 1rem;
resize: vertical;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
form input[type="text"]:focus,
form textarea:focus {
outline: none;
border-color: #8e2de2;
box-shadow: 0 0 5px rgba(142,45,226,0.5);
}
/* --- Boutons Publier et Annuler --- */
.form-buttons {
display: flex;
justify-content: center;
gap: 15px;
}
button,
a.btn {
padding: 12px 25px;
background: linear-gradient(135deg, #6a11cb, #8e2de2);
color: #fff;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: background 0.3s ease, transform 0.2s ease;
}
button:hover,
a.btn:hover {
background: linear-gradient(135deg, #8e2de2, #6a11cb);
transform: translateY(-2px);
}
/* --- Messages d'erreur --- */
.error {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
padding: 12px;
border-radius: 6px;
width: 90%;
max-width: 600px;
margin-bottom: 15px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
/* --- Footer violet --- */
footer {
background: #6a11cb;
color: #fff;
text-align: center;
padding: 15px 0;
width: 100%;
box-shadow: 0 -2px 5px rgba(0,0,0,0.2);
font-size: 0.95rem;
position: fixed;
bottom: 0;
}

137
assets/style.css Normal file
View File

@@ -0,0 +1,137 @@
/* --- Style global --- */
body {
font-family: 'Segoe UI', Roboto, Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
background-color: #f3f0f9;
color: #333;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* --- En-tête --- */
header {
background: linear-gradient(135deg, #6a11cb, #8e2de2); /* Dégradé violet */
color: #fff;
padding: 15px 30px;
display: flex;
justify-content: space-between; /* Accueil à gauche, Admin à droite */
align-items: center;
position: fixed; /* Fixé en haut */
top: 0;
left: 0;
width: 100%;
z-index: 1000;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
/* --- Liens dans le header --- */
header a {
color: #fff;
text-decoration: none;
font-weight: 600;
font-size: 1.1rem;
transition: opacity 0.2s ease;
}
header a:hover {
opacity: 0.8;
}
/* --- Espace pour éviter que le contenu passe sous le header --- */
main {
flex: 1;
margin-top: 80px; /* Hauteur du header */
padding: 20px;
}
/* --- Titres --- */
h1, h2, h3 {
color: #5b1dbb;
}
/* --- Articles --- */
article {
background: #fff;
padding: 20px;
margin: 15px auto;
width: 90%;
max-width: 800px;
border-radius: 10px;
border: 1px solid #e0d4f7;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
article:hover {
transform: translateY(-3px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
}
/* --- Liens darticles --- */
article h2 a {
color: #8e2de2;
text-decoration: none;
transition: color 0.2s ease;
}
article h2 a:hover {
color: #5b1dbb;
text-decoration: underline;
}
/* --- Tableaux --- */
table {
width: 90%;
margin: 25px auto;
border-collapse: collapse;
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
table th, table td {
padding: 12px 15px;
border-bottom: 1px solid #ddd;
}
table th {
background-color: #8e2de2;
color: #fff;
text-align: left;
}
table tr:hover {
background-color: #f5e9ff;
}
/* --- Messages derreur --- */
.error {
color: #721c24;
font-weight: 600;
border: 1px solid #f5c6cb;
background: #f8d7da;
padding: 12px;
border-radius: 6px;
width: 90%;
max-width: 800px;
margin: 10px auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* --- Pied de page --- */
footer {
background: #6a11cb;
color: #fff;
text-align: center;
padding: 15px 0;
position: fixed; /* Collé en bas */
bottom: 0;
left: 0;
width: 100%;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2);
font-size: 0.95rem;
}

8
bdd/Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
FROM mysql
LABEL authors="abdr-cms"
ENV MYSQL_ROOT_PASSWORD='passwordroot'
COPY ./init.sql /docker-entrypoint-initdb.d/
EXPOSE 3306

20
bdd/init.sql Normal file
View File

@@ -0,0 +1,20 @@
CREATE DATABASE IF NOT EXISTS cms_simple CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE cms_simple;
CREATE TABLE utilisateur (
id INT AUTO_INCREMENT PRIMARY KEY,
login VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL
);
CREATE TABLE articles (
id INT AUTO_INCREMENT PRIMARY KEY,
titre VARCHAR(255) NOT NULL,
contenu TEXT NOT NULL,
date_creation DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO utilisateur (login, password) VALUES ('user', '$2y$10$W.kLJXFNZPdYcKXQbIyAVO0BGJQcVFc6JGYtsa1Z7SIjRDyUoOIfG');

32
include/authenticator.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
session_start();
function isLogged(): bool
{
return isset($_SESSION['user']);
}
function checkLogin(PDO $pdo, $login, $password): bool
{
$stmt = $pdo->prepare('SELECT * FROM utilisateur WHERE login = ?');
$stmt->execute([$login]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user'] = $user['login'];
return true;
}
return false;
}
function requireLogin() {
if (!isLogged()) {
header('Location: login.php');
exit;
}
}
?>

12
include/db.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
$host = 'CMS_sql'; // nom du conteneur MySQL (dans le réseau Docker)
$dbname = 'cms_simple';
$user = 'root';
$pass = 'passwordroot';
try {
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die('Erreur de connexion : ' . $e->getMessage());
}

42
index.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
require 'include/db.php';
$stmt = $pdo->query('SELECT * FROM articles ORDER BY date_creation DESC LIMIT 10');
$articles = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Accueil - CMS</title>
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<header>
<nav>
<a href="index.php">Accueil</a> |
<a href="admin/login.php">Administratreur</a>
</nav>
</header>
<main>
<h1>Articles</h1>
<?php if (empty($articles)): ?>
<p>Aucun Article pour le moment</p>
<?php endif; ?>
<?php foreach ($articles as $a): ?>
<article>
<h2><a href="article.php?id=<?= $a['id'] ?>"><?= htmlspecialchars($a['titre']) ?></a></h2>
<small>Publié le <?= date('d/m/Y', strtotime($a['date_creation'])) ?></small>
<p><?= substr(htmlspecialchars($a['contenu']), 0, 200) ?>...</p>
<p><a href="article.php?id=<?= $a['id'] ?>">Lire la suite...</a></p>
</article>
<?php endforeach; ?>
</main>
<footer>
<p>&copy; <?= date('Y') ?> CMS. Tous droits réservés par Abd'R.</p>
</footer>
</body>
</html>

25
launch-dockers.sh Normal file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
docker stop CMS_sql
docker stop CMS_php
docker rm CMS_sql
docker rm CMS_php
docker network rm CMS-bridge
docker network create -d bridge CMS-bridge
cd bdd
docker build -t cms_sql .
docker run -d --name CMS_sql -p 3307:3306 --network=CMS-bridge cms_sql:latest
cd ..
docker build -t cms_php .
docker run -d --name CMS_php -p 8080:80 --network=CMS-bridge cms_php:latest