diff --git a/database/scriptDatabase.sql b/database/scriptDatabase.sql index ae6434c..3b94c1f 100644 --- a/database/scriptDatabase.sql +++ b/database/scriptDatabase.sql @@ -57,4 +57,52 @@ values ('2026-06-08 11:10:21', 11, '2026-06-08 13:10:41', '$2y$12$x01fDqrHQEYIyj ALTER TABLE projects ADD COLUMN status VARCHAR(100) NULL AFTER title; ALTER TABLE experiences -ADD COLUMN status VARCHAR(100) NULL AFTER title; \ No newline at end of file +ADD COLUMN status VARCHAR(100) NULL AFTER title; + +-- Table de réglages du site (clé / valeur) : utilisée pour la notification de la page d'accueil +CREATE TABLE IF NOT EXISTS settings ( + name VARCHAR(50) NOT NULL, + value TEXT NULL, + PRIMARY KEY (name) +); + +-- Valeurs par défaut de la notification (modifiables ensuite depuis le panel Admin) +INSERT INTO settings (name, value) VALUES +('site_notice_title', 'Des bugs peuvent encore se produire.'), +('site_notice_text', 'Si le CSS ou les images s''affichent mal, pensez à vider le cache de votre navigateur (Ctrl + F5 ou Cmd + Shift + R) pour voir les dernières modifications.'), +('site_notice_enabled', '1') +ON DUPLICATE KEY UPDATE value = VALUES(value); + +-- Table des compétences : une ligne = une catégorie (carte sur la page Compétences) +-- items : un item par ligne, au format "classes-icone-fontawesome|Texte" (l'icône est optionnelle : "Texte" seul fonctionne aussi) +CREATE TABLE IF NOT EXISTS skills ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + title VARCHAR(100) NOT NULL, + icon VARCHAR(100) NULL, + items TEXT NOT NULL, + position INT NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- Compétences actuelles (reprises du CV) +INSERT INTO skills (title, icon, items, position) VALUES +('Systèmes d''exploitation', 'fa-solid fa-computer', 'fa-brands fa-microsoft|Windows (Serveur/XP/8/10/11)\nfa-brands fa-linux|Linux (Debian/Ubuntu)\nfa-brands fa-apple|macOS', 1), +('Réseaux', 'fa-solid fa-network-wired', 'fa-solid fa-location-dot|Adressage IP\nfa-solid fa-diagram-project|VLAN\nfa-solid fa-shield-halved|Pare-feu (iptables/ufw)\nfa-solid fa-globe|DNS (Bind9)\nfa-solid fa-server|DHCP (ISC DHCP)\nfa-solid fa-arrows-left-right|Proxy (Squid)', 2), +('Scripting', 'fa-solid fa-terminal', 'fa-solid fa-terminal|Bash\nfa-brands fa-python|Python\nfa-brands fa-java|Java\nfa-solid fa-gears|Ansible', 3), +('Applications', 'fa-solid fa-cubes', 'fa-solid fa-address-book|LDAP (Windows AD)\nfa-solid fa-headset|GLPI\nfa-solid fa-compact-disc|FOG\nfa-solid fa-server|Nginx / Apache\nfa-brands fa-docker|Docker\nfa-solid fa-display|Guacamole\nfa-solid fa-database|MySQL / MariaDB', 4), +('Développement web', 'fa-solid fa-code', 'fa-brands fa-html5|HTML\nfa-brands fa-css3-alt|CSS\nfa-brands fa-js|JavaScript\nfa-brands fa-php|PHP', 5), +('Virtualisation & Outils', 'fa-solid fa-layer-group', 'fa-solid fa-server|Proxmox\nfa-solid fa-box|VirtualBox\nfa-brands fa-git-alt|Git (GitHub, Gitea)', 6); + +-- Table des écoles / formations : affichée sous les expériences professionnelles sur la page Expériences +CREATE TABLE IF NOT EXISTS schools ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + title VARCHAR(100) NOT NULL, + status VARCHAR(100) NULL, + tags VARCHAR(255) NULL, + description TEXT NOT NULL, + duration VARCHAR(100) NOT NULL, + image_url VARCHAR(255) NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/elements/footer.php b/elements/footer.php index 8d92ae0..5db71bd 100644 --- a/elements/footer.php +++ b/elements/footer.php @@ -20,5 +20,37 @@ + + + \ No newline at end of file diff --git a/functions/listSchool.php b/functions/listSchool.php new file mode 100644 index 0000000..0f7a5b6 --- /dev/null +++ b/functions/listSchool.php @@ -0,0 +1,116 @@ +prepare('SELECT id, title, status, tags, description, duration, image_url FROM schools ORDER BY id DESC'); + $stmt->execute(); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + return []; + } +} + +// Fonction qui permet de récupérer une école par id (pour la modification dans le panel Admin) +function recupererEcoleParId($id) { + global $dbh; + + try { + $stmt = $dbh->prepare('SELECT id, title, status, tags, description, duration, image_url FROM schools WHERE id = :id'); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + return $stmt->fetch(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + return false; + } +} + +// Fonction interne qui affiche une carte école (utilisée par la page publique et le panel admin) +function afficherEcoleCard($ecole, $admin = false) { + $id = (int) $ecole['id']; + $titre = htmlspecialchars($ecole['title']); + $description = nl2br(htmlspecialchars($ecole['description'])); + $duration = htmlspecialchars($ecole['duration']); + $image = htmlspecialchars($ecole['image_url']); + $status = htmlspecialchars($ecole['status'] ?? ''); + $tags = []; + + // S'il y a des tags alors on les sépares grâce à la virgule (tags1,tags2,etc...) + if (!empty($ecole['tags'])) { + $tags = array_filter(array_map('trim', explode(',', $ecole['tags']))); + } + + echo '
'; + + // Si la donnée avec l'image n'est pas null alors on l'affiche + if (!empty($image)) { + echo '
'; + echo 'Image de l ecole ' . $titre . ''; + + if (!empty($status)) { + echo '
' . $status . '
'; + } + + echo '
'; + } + + echo '
'; + echo '

' . $titre . '

'; + + echo '

Période : ' . $duration . '

'; + + if (!empty($tags)) { + echo '
'; + foreach ($tags as $tag) { + echo '' . htmlspecialchars($tag) . ''; + } + echo '
'; + } + + echo '

' . $description . '

'; + + // Boutons de modification / suppression pour le panel admin + if ($admin) { + echo '
'; + echo 'Modifier'; + echo 'Supprimer'; + echo '
'; + } + + echo '
'; + echo '
'; +} + +// Fonction qui va afficher les écoles sur la page publique +function afficherEcoles() { + $ecoles = listeEcoles(); + + // Si la table est vide ou n'existe pas encore on n'affiche rien + if (empty($ecoles)) { + return; + } + + echo '
'; + foreach ($ecoles as $ecole) { + afficherEcoleCard($ecole, false); + } + echo '
'; +} + +// Fonction identique à celle en haut mais ajout de boutons pour le panel admin +function afficherEcolesAdmin() { + $ecoles = listeEcoles(); + + echo '
'; + foreach ($ecoles as $ecole) { + afficherEcoleCard($ecole, true); + } + echo '
'; +} diff --git a/functions/listSettings.php b/functions/listSettings.php new file mode 100644 index 0000000..0f3bcc3 --- /dev/null +++ b/functions/listSettings.php @@ -0,0 +1,48 @@ +prepare('SELECT value FROM settings WHERE name = :name'); + $stmt->bindValue(':name', $name, PDO::PARAM_STR); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + return $row ? $row['value'] : null; + } catch (PDOException $e) { + return null; + } +} + +// Fonction qui affiche la notification de la page d'accueil (si elle est activée) +function afficherNotice() { + $enabled = recupererSetting('site_notice_enabled'); + $titre = recupererSetting('site_notice_title'); + $texte = recupererSetting('site_notice_text'); + + // Valeurs par défaut tant que la table settings n'existe pas encore + if ($titre === null && $texte === null) { + $enabled = '1'; + $titre = 'Des bugs peuvent encore se produire.'; + $texte = "Si le CSS ou les images s'affichent mal, pensez à vider le cache de votre navigateur (Ctrl + F5 ou Cmd + Shift + R) pour voir les dernières modifications."; + } + + // Si la notification est désactivée ou vide on n'affiche rien + if ($enabled !== '1' || (empty($titre) && empty($texte))) { + return; + } + + echo '

'; + if (!empty($titre)) { + echo '' . htmlspecialchars($titre) . ' '; + } + echo nl2br(htmlspecialchars($texte)); + echo '

'; +} diff --git a/functions/listSkills.php b/functions/listSkills.php new file mode 100644 index 0000000..466eefa --- /dev/null +++ b/functions/listSkills.php @@ -0,0 +1,98 @@ +prepare('SELECT id, title, icon, items, position FROM skills ORDER BY position ASC, id ASC'); + $stmt->execute(); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + return []; + } +} + +// Fonction qui permet de récupérer une catégorie de compétences par id (pour la modification dans le panel Admin) +function recupererSkillParId($id) { + global $dbh; + + try { + $stmt = $dbh->prepare('SELECT id, title, icon, items, position FROM skills WHERE id = :id'); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + return $stmt->fetch(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + return false; + } +} + +// Fonction interne qui affiche une carte de compétences (utilisée par la page publique et le panel admin) +// Les items sont stockés un par ligne, au format "classes-icone|Texte" (icône optionnelle) +function afficherSkillCard($skill, $admin = false) { + $id = (int) $skill['id']; + $titre = htmlspecialchars($skill['title']); + $icone = htmlspecialchars(!empty($skill['icon']) ? $skill['icon'] : 'fa-solid fa-star'); + + echo '
'; + echo '
'; + echo '

' . $titre . '

'; + echo ''; + + // Boutons de modification / suppression pour le panel admin + if ($admin) { + echo '
'; + echo 'Modifier'; + echo 'Supprimer'; + echo '
'; + } + + echo '
'; +} + +// Fonction qui va afficher les compétences sur la page publique +function afficherSkills() { + $skills = listeSkills(); + + // Si la table est vide ou n'existe pas encore + if (empty($skills)) { + echo '

Les compétences seront bientôt disponibles.

'; + return; + } + + echo '
'; + foreach ($skills as $skill) { + afficherSkillCard($skill, false); + } + echo '
'; +} + +// Fonction identique à celle en haut mais ajout de boutons pour le panel admin +function afficherSkillsAdmin() { + $skills = listeSkills(); + + echo '
'; + foreach ($skills as $skill) { + afficherSkillCard($skill, true); + } + echo '
'; +} diff --git a/pages/admin.php b/pages/admin.php index 321fc59..a734413 100644 --- a/pages/admin.php +++ b/pages/admin.php @@ -10,6 +10,9 @@ require_once '../functions/databaseConnection.php'; require_once("../functions/listUser.php"); require_once('../functions/listProject.php'); require_once('../functions/listXP.php'); +require_once('../functions/listSchool.php'); +require_once('../functions/listSkills.php'); +require_once('../functions/listSettings.php'); // L'utilisateur doit être administrateur @@ -21,15 +24,28 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') { // On met les variables à null ici pour éviter qu'il garde les options en premier chargement $projetAModifier = null; $xpAModifier = null; +$ecoleAModifier = null; +$skillAModifier = null; -// Si on modifie XP ou Projets +// Si on modifie XP, Projets, École ou Compétence if (isset($_GET['edit_project']) && is_numeric($_GET['edit_project'])) { $projetAModifier = recupererProjetParId((int) $_GET['edit_project']); } if (isset($_GET['edit_xp']) && is_numeric($_GET['edit_xp'])) { $xpAModifier = recupererXPParId((int) $_GET['edit_xp']); } +if (isset($_GET['edit_school']) && is_numeric($_GET['edit_school'])) { + $ecoleAModifier = recupererEcoleParId((int) $_GET['edit_school']); +} +if (isset($_GET['edit_skill']) && is_numeric($_GET['edit_skill'])) { + $skillAModifier = recupererSkillParId((int) $_GET['edit_skill']); +} + +// Valeurs actuelles de la notification de la page d'accueil +$noticeTitle = recupererSetting('site_notice_title') ?? ''; +$noticeText = recupererSetting('site_notice_text') ?? ''; +$noticeEnabled = recupererSetting('site_notice_enabled') ?? '1'; // Page du panneau d'administration ?> @@ -103,7 +119,105 @@ if (isset($_GET['edit_xp']) && is_numeric($_GET['edit_xp'])) { - + + + + + + +
+
+ +
+

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+
+ +
+

+
+ + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+
+ +
+

Notification de la page d'accueil

+
+ + +
+ + + + + + + + + + + +
@@ -116,7 +230,14 @@ if (isset($_GET['edit_xp']) && is_numeric($_GET['edit_xp'])) {

Gérer les projets

Gérer les experiences

- +

Gérer les écoles

+ +

Gérer les compétences

+
+ +
+ diff --git a/pages/home.php b/pages/home.php index 37bdf0b..1af3ac2 100644 --- a/pages/home.php +++ b/pages/home.php @@ -9,17 +9,12 @@ $pageTitle = 'Accueil'; // Insertion du header require_once('./../elements/header.php'); -// Ajout d'un texte pour rappeller aux utilisateurs de la démo de vider le cache si du CSS s'affiche mal - -//Si du CSS / ou des images s'affichent mal, il faut penser à vider le cache, version de développement uniquement. +// La notification est maintenant dynamique : modifiable depuis le panel Admin (table settings) +require_once('../functions/listSettings.php'); +// Affichage de la notification (si elle est activée dans le panel Admin) +afficherNotice(); ?> - -

- Version de développement uniquement. - Si le CSS ou les images s'affichent mal, pensez à vider le cache de votre navigateur - (Ctrl + F5 ou Cmd + Shift + R) pour voir les dernières modifications. -

diff --git a/pages/skills.php b/pages/skills.php index d64de94..1a628b0 100644 --- a/pages/skills.php +++ b/pages/skills.php @@ -1,6 +1,6 @@ Vous êtes administrateur, vous pouvez modifier les compétences ici.

"; +} + ?> @@ -18,96 +23,12 @@ require_once('./../elements/header.php');

Un aperçu des technologies et des domaines que je maîtrise.

-
+ -
-
- -
-

Systèmes d'exploitation

-
    -
  • Windows (Serveur/XP/8/10/11)
  • -
  • Linux (Debian/Ubuntu)
  • -
  • macOS
  • -
-
- - -
-
- -
-

Réseaux

-
    -
  • Adressage IP
  • -
  • VLAN
  • -
  • Pare-feu (iptables/ufw)
  • -
  • DNS (Bind9)
  • -
  • DHCP (ISC DHCP)
  • -
  • Proxy (Squid)
  • -
-
- - -
-
- -
-

Scripting

-
    -
  • Bash
  • -
  • Python
  • -
  • Java
  • -
  • Ansible
  • -
-
- - -
-
- -
-

Applications

-
    -
  • LDAP (Windows AD)
  • -
  • GLPI
  • -
  • FOG
  • -
  • Nginx / Apache
  • -
  • Docker
  • -
  • Guacamole
  • -
  • MySQL / MariaDB
  • -
-
- - -
-
- -
-

Développement web

-
    -
  • HTML
  • -
  • CSS
  • -
  • JavaScript
  • -
  • PHP
  • -
-
- - -
-
- -
-

Virtualisation & Outils

-
    -
  • Proxmox
  • -
  • VirtualBox
  • -
  • Git (GitHub, Gitea)
  • -
-
- -
+ // Appel de ma fonction dans Functions/listSkills.php + afficherSkills(); + ?>
diff --git a/pages/xp.php b/pages/xp.php index 99225d4..b872fc5 100644 --- a/pages/xp.php +++ b/pages/xp.php @@ -27,6 +27,22 @@ require_once('../functions/listXP.php'); // Appel de ma fonction dans Functions/listXP.php afficherXP(); +require_once('../functions/listSchool.php'); + +// Section écoles / formations, en dessous des expériences professionnelles +// On n'affiche le titre que s'il y a au moins une école (table remplie) +if (!empty(listeEcoles())) { +?> + + + + diff --git a/scripts/addSchool.php b/scripts/addSchool.php new file mode 100644 index 0000000..ff64230 --- /dev/null +++ b/scripts/addSchool.php @@ -0,0 +1,54 @@ +prepare('INSERT INTO schools (title, tags, description, duration, image_url) VALUES (:title, :tags, :description, :duration, :image_url)'); + + // On execute la requete préparée + $stmt->execute([ + ':title' => $title, + ':tags' => $tags, + ':description' => $description, + ':duration' => $duration, + ':image_url' => $image_url + ]); + + // Redirection vers xp.php dès que la requete est executée + header('Location: ../pages/xp.php'); + exit; + + } catch (PDOException $e) { + // En cas d'erreur : + echo "Erreur pendant l'ajout de l'école: " . $e->getMessage(); + exit; + } + } else { + echo "Veuillez remplir tous les champs obligatoires."; + exit; + } +} else { + // Si on y accède de façon peu légitime sans requête POST on refuse + header('Location: ../pages/xp.php'); + exit; +} +?> diff --git a/scripts/addSkill.php b/scripts/addSkill.php new file mode 100644 index 0000000..47c8c31 --- /dev/null +++ b/scripts/addSkill.php @@ -0,0 +1,52 @@ +prepare('INSERT INTO skills (title, icon, items, position) VALUES (:title, :icon, :items, :position)'); + + // On execute la requete préparée + $stmt->execute([ + ':title' => $title, + ':icon' => $icon, + ':items' => $items, + ':position' => $position + ]); + + // Redirection vers skills.php dès que la requete est executée + header('Location: ../pages/skills.php'); + exit; + + } catch (PDOException $e) { + // En cas d'erreur : + echo "Erreur pendant l'ajout de la compétence: " . $e->getMessage(); + exit; + } + } else { + echo "Veuillez remplir tous les champs obligatoires."; + exit; + } +} else { + // Si on y accède de façon peu légitime sans requête POST on refuse + header('Location: ../pages/skills.php'); + exit; +} +?> diff --git a/scripts/removeSchool.php b/scripts/removeSchool.php new file mode 100644 index 0000000..d2249b3 --- /dev/null +++ b/scripts/removeSchool.php @@ -0,0 +1,31 @@ +prepare('DELETE FROM schools WHERE id = :id'); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + $stmt->execute(); // Sinon on renvoie l'erreur +} catch (PDOException $e) { + die("Erreur lors de la suppression de l'école :" . $e->getMessage()); +} +// On renvoie l'utilisateur sur admin.php +header('Location: ../pages/admin.php'); +exit; diff --git a/scripts/removeSkill.php b/scripts/removeSkill.php new file mode 100644 index 0000000..02f452f --- /dev/null +++ b/scripts/removeSkill.php @@ -0,0 +1,31 @@ +prepare('DELETE FROM skills WHERE id = :id'); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + $stmt->execute(); // Sinon on renvoie l'erreur +} catch (PDOException $e) { + die("Erreur lors de la suppression de la compétence :" . $e->getMessage()); +} +// On renvoie l'utilisateur sur admin.php +header('Location: ../pages/admin.php'); +exit; diff --git a/scripts/updateSchool.php b/scripts/updateSchool.php new file mode 100644 index 0000000..e6e507d --- /dev/null +++ b/scripts/updateSchool.php @@ -0,0 +1,50 @@ +prepare(' + UPDATE schools + SET title = :title, tags = :tags, description = :description, duration = :duration, image_url = :image_url + WHERE id = :id + '); + $stmt->bindValue(':title', $title, PDO::PARAM_STR); + $stmt->bindValue(':tags', $tags, PDO::PARAM_STR); + $stmt->bindValue(':description', $description, PDO::PARAM_STR); + $stmt->bindValue(':duration', $duration, PDO::PARAM_STR); + $stmt->bindValue(':image_url', $imageurl, PDO::PARAM_STR); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + $stmt->execute(); // Sinon on renvoie l'erreur +} catch (PDOException $e) { + die("Erreur lors de la modification de l'école :" . $e->getMessage()); +} +// On renvoie l'utilisateur sur admin.php +header('Location: ../pages/admin.php'); +exit; diff --git a/scripts/updateSettings.php b/scripts/updateSettings.php new file mode 100644 index 0000000..6ac68a6 --- /dev/null +++ b/scripts/updateSettings.php @@ -0,0 +1,43 @@ + trim($_POST['notice_title'] ?? ''), + 'site_notice_text' => trim($_POST['notice_text'] ?? ''), + 'site_notice_enabled' => isset($_POST['notice_enabled']) ? '1' : '0' +]; + +try { // On essaie d'executer une requête qui est préparée pour chaque réglage + // ON DUPLICATE KEY UPDATE : crée le réglage s'il n'existe pas encore, le met à jour sinon + $stmt = $dbh->prepare('INSERT INTO settings (name, value) VALUES (:name, :value) ON DUPLICATE KEY UPDATE value = VALUES(value)'); + + foreach ($reglages as $name => $value) { + $stmt->execute([ + ':name' => $name, + ':value' => $value + ]); + } +} catch (PDOException $e) { + die("Erreur lors de la modification de la notification :" . $e->getMessage()); +} + +// On renvoie l'utilisateur sur admin.php +header('Location: ../pages/admin.php'); +exit; diff --git a/scripts/updateSkill.php b/scripts/updateSkill.php new file mode 100644 index 0000000..83772c4 --- /dev/null +++ b/scripts/updateSkill.php @@ -0,0 +1,46 @@ +prepare(' + UPDATE skills + SET title = :title, icon = :icon, items = :items, position = :position + WHERE id = :id + '); + $stmt->bindValue(':title', $title, PDO::PARAM_STR); + $stmt->bindValue(':icon', $icon, PDO::PARAM_STR); + $stmt->bindValue(':items', $items, PDO::PARAM_STR); + $stmt->bindValue(':position', $position, PDO::PARAM_INT); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + $stmt->execute(); // Sinon on renvoie l'erreur +} catch (PDOException $e) { + die("Erreur lors de la modification de la compétence :" . $e->getMessage()); +} +// On renvoie l'utilisateur sur admin.php +header('Location: ../pages/admin.php'); +exit; diff --git a/styles/primary_css.css b/styles/primary_css.css index 762e94c..6812e3f 100644 --- a/styles/primary_css.css +++ b/styles/primary_css.css @@ -9,11 +9,11 @@ --surface-3: #202030; --border: #262638; --border-strong: #34344a; - --accent: #6c63ff; - --accent-strong: #574fe0; - --accent-light: #8f88ff; - --accent-soft: rgba(108, 99, 255, 0.14); - --accent-border: rgba(108, 99, 255, 0.32); + --accent: #ff7a1a; + --accent-strong: #e8630a; + --accent-light: #ffa155; + --accent-soft: rgba(255, 122, 26, 0.14); + --accent-border: rgba(255, 122, 26, 0.32); --text: #f2f2f8; --text-soft: #c7c7d6; --text-muted: #9b9bb2; @@ -25,9 +25,9 @@ --radius-lg: 22px; --shadow-sm: 0 4px 14px rgba(0, 0, 0, 0.25); --shadow: 0 14px 34px rgba(0, 0, 0, 0.35); - --shadow-accent: 0 14px 34px rgba(108, 99, 255, 0.22); - --glow-1: rgba(108, 99, 255, 0.14); - --glow-2: rgba(108, 99, 255, 0.07); + --shadow-accent: 0 14px 34px rgba(255, 122, 26, 0.22); + --glow-1: rgba(255, 122, 26, 0.14); + --glow-2: rgba(255, 122, 26, 0.07); --header-bg: rgba(12, 12, 18, 0.78); --nav-bg: rgba(12, 12, 18, 0.97); --footer-bg: rgba(12, 12, 18, 0.6); @@ -44,17 +44,17 @@ --surface-3: #e9e9f2; --border: #e3e3ee; --border-strong: #cfcfe0; - --accent-light: #5a51e8; - --accent-soft: rgba(108, 99, 255, 0.1); - --accent-border: rgba(108, 99, 255, 0.3); + --accent-light: #d95d00; + --accent-soft: rgba(255, 122, 26, 0.1); + --accent-border: rgba(255, 122, 26, 0.3); --text: #191924; --text-soft: #3c3c4e; --text-muted: #6d6d84; --shadow-sm: 0 4px 14px rgba(30, 30, 60, 0.08); --shadow: 0 14px 34px rgba(30, 30, 60, 0.12); - --shadow-accent: 0 14px 34px rgba(108, 99, 255, 0.18); - --glow-1: rgba(108, 99, 255, 0.1); - --glow-2: rgba(108, 99, 255, 0.05); + --shadow-accent: 0 14px 34px rgba(255, 122, 26, 0.18); + --glow-1: rgba(255, 122, 26, 0.1); + --glow-2: rgba(255, 122, 26, 0.05); --header-bg: rgba(255, 255, 255, 0.82); --nav-bg: rgba(255, 255, 255, 0.98); --footer-bg: rgba(255, 255, 255, 0.7); @@ -181,7 +181,7 @@ img { margin-left: 8px; color: white; background: linear-gradient(180deg, var(--accent) 0%, var(--accent-strong) 100%); - box-shadow: 0 6px 18px rgba(108, 99, 255, 0.3); + box-shadow: 0 6px 18px rgba(255, 122, 26, 0.3); } .site-nav a.nav-cta:hover { @@ -306,15 +306,15 @@ img { max-width: 900px; width: calc(100% - 32px); border-radius: 14px; - background: linear-gradient(135deg, rgba(255, 196, 0, 0.14), rgba(255, 140, 0, 0.08)); - border: 1px solid rgba(255, 196, 0, 0.3); - color: #ffeeba; + background: linear-gradient(135deg, rgba(108, 99, 255, 0.16), rgba(87, 79, 224, 0.1)); + border: 1px solid rgba(108, 99, 255, 0.35); + color: #dcd9ff; font-size: 0.95rem; line-height: 1.6; } .dev-cache-notice strong { - color: #ffd86b; + color: #b7b1ff; } .dev-cache-notice::before { @@ -325,13 +325,13 @@ img { /* Version thème clair du bandeau d'avertissement */ [data-theme="light"] .dev-cache-notice { - background: linear-gradient(135deg, rgba(255, 196, 0, 0.16), rgba(255, 140, 0, 0.1)); - border-color: rgba(190, 130, 0, 0.35); - color: #7a5b00; + background: linear-gradient(135deg, rgba(108, 99, 255, 0.12), rgba(87, 79, 224, 0.07)); + border-color: rgba(108, 99, 255, 0.35); + color: #4a43c0; } [data-theme="light"] .dev-cache-notice strong { - color: #8a6400; + color: #3a34a8; } /* ---- Message admin en haut des listes ---- */ @@ -383,12 +383,75 @@ img { line-height: 1.1; } +/* Reflet lumineux qui balaye le prénom en continu */ .hero h1 .accent { - background: linear-gradient(120deg, var(--accent-light), var(--accent)); + background: linear-gradient( + 120deg, + var(--accent-light) 0%, + var(--accent) 35%, + #ffd9b3 50%, + var(--accent) 65%, + var(--accent-light) 100% + ); + background-size: 200% auto; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent; + animation: heroShimmer 4s linear infinite; +} + +/* Apparition en cascade des éléments du hero au chargement */ +.hero-badge, +.hero h1, +.hero p, +.hero-actions { + opacity: 0; + animation: heroFadeUp 0.7s ease forwards; +} + +.hero h1 { + animation-delay: 0.15s; +} + +.hero p { + animation-delay: 0.3s; +} + +.hero-actions { + animation-delay: 0.45s; +} + +@keyframes heroFadeUp { + from { + opacity: 0; + transform: translateY(18px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes heroShimmer { + to { + background-position: -200% center; + } +} + +/* Désactive les animations si l'utilisateur préfère un affichage sans mouvement */ +@media (prefers-reduced-motion: reduce) { + .hero-badge, + .hero h1, + .hero p, + .hero-actions { + animation: none; + opacity: 1; + } + + .hero h1 .accent { + animation: none; + } } .hero p { @@ -421,13 +484,13 @@ img { .btn-primary { color: white; background: linear-gradient(180deg, var(--accent) 0%, var(--accent-strong) 100%); - box-shadow: 0 10px 26px rgba(108, 99, 255, 0.32); + box-shadow: 0 10px 26px rgba(255, 122, 26, 0.32); } .btn-primary:hover { transform: translateY(-2px); filter: brightness(1.08); - box-shadow: 0 14px 32px rgba(108, 99, 255, 0.4); + box-shadow: 0 14px 32px rgba(255, 122, 26, 0.4); } .btn-ghost { @@ -449,7 +512,8 @@ img { padding: 0 clamp(16px, 4vw, 40px); } -.page-header h1 { +.page-header h1, +.page-header h2 { margin: 0 0 8px; font-size: clamp(28px, 4.5vw, 42px); font-weight: 800; @@ -534,7 +598,7 @@ img { .login-card input[type="password"]:focus { outline: none; border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.2); + box-shadow: 0 0 0 3px rgba(255, 122, 26, 0.2); } .login-card input::placeholder { @@ -554,7 +618,7 @@ img { font-weight: 600; cursor: pointer; margin-top: 8px; - box-shadow: 0 10px 24px rgba(108, 99, 255, 0.28); + box-shadow: 0 10px 24px rgba(255, 122, 26, 0.28); transition: transform 0.2s ease, filter 0.2s ease; } @@ -663,7 +727,7 @@ img { .project-form-card textarea:focus { outline: none; border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.18); + box-shadow: 0 0 0 3px rgba(255, 122, 26, 0.18); } .project-form-card textarea { @@ -700,6 +764,24 @@ img { } } +/* Case à cocher (formulaire notification du panel admin) */ +.checkbox-row { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 16px; + color: var(--text-soft); + font-size: 14px; + cursor: pointer; +} + +.checkbox-row input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--accent); + cursor: pointer; +} + /* Bouton violet */ .btn-submit { background: linear-gradient(180deg, var(--accent) 0%, var(--accent-strong) 100%); @@ -935,7 +1017,7 @@ img { .contact-info { background: - radial-gradient(400px 200px at 20% 0%, rgba(108, 99, 255, 0.16), transparent 70%), + radial-gradient(400px 200px at 20% 0%, rgba(255, 122, 26, 0.16), transparent 70%), var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); @@ -1008,7 +1090,7 @@ img { .contact-form textarea:focus { outline: none; border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.18); + box-shadow: 0 0 0 3px rgba(255, 122, 26, 0.18); background-color: var(--surface-3); } @@ -1024,7 +1106,7 @@ img { font-family: var(--font); font-weight: 600; cursor: pointer; - box-shadow: 0 10px 24px rgba(108, 99, 255, 0.28); + box-shadow: 0 10px 24px rgba(255, 122, 26, 0.28); transition: filter 0.2s ease, transform 0.2s ease; } @@ -1262,6 +1344,17 @@ img { text-align: center; } +/* Boutons admin dans les cartes de compétences */ +.skill-category .admin-actions { + margin-top: 18px; +} + +/* Grille des compétences dans le panel admin : moins d'espace vertical que la page publique */ +.skills-section-admin { + padding-top: 0; + padding-bottom: 24px; +} + /* ---- PAGE CGU ---- */ .cgu-page { max-width: 980px; @@ -1321,3 +1414,147 @@ img { .cgu-card br { display: none; } + +/* ==== ANIMATIONS & INTERACTIONS ==== */ + +/* Transition entre les pages : fondu de l'ancienne page vers la nouvelle + (View Transitions API, navigateurs récents uniquement, sans effet ailleurs) */ +@view-transition { + navigation: auto; +} + +::view-transition-old(root) { + animation: pageOut 0.2s ease both; +} + +::view-transition-new(root) { + animation: pageIn 0.3s ease both; +} + +@keyframes pageOut { + to { + opacity: 0; + } +} + +@keyframes pageIn { + from { + opacity: 0; + transform: translateY(10px); + } +} + +/* Entrée du contenu à chaque chargement de page (pour les navigateurs sans View Transitions) */ +main { + animation: pageEnter 0.45s ease; +} + +@keyframes pageEnter { + from { + opacity: 0; + transform: translateY(12px); + } +} + +/* Si le navigateur gère les View Transitions on évite de cumuler les deux entrées */ +@supports (view-transition-name: root) { + main { + animation: none; + } +} + +/* Apparition au défilement : la classe .reveal est posée en JS (footer), + puis .visible est ajoutée quand l'élément entre dans l'écran */ +.reveal { + opacity: 0; + transform: translateY(22px); + transition: opacity 0.55s ease, transform 0.55s ease; + transition-delay: var(--reveal-delay, 0s); +} + +.reveal.visible { + opacity: 1; + transform: translateY(0); +} + +/* ---- Micro-interactions ---- */ + +/* Liens de navigation : légère élévation au survol */ +.site-nav a { + transition: color 0.2s ease, background-color 0.2s ease, transform 0.2s ease; +} + +.site-nav a:hover { + transform: translateY(-1px); +} + +/* Icône du bouton de thème : rotation au survol */ +.theme-toggle i { + transition: transform 0.35s ease; +} + +.theme-toggle:hover i { + transform: rotate(40deg) scale(1.12); +} + +/* Tags : petite élévation au survol */ +.tag { + transition: transform 0.2s ease, background-color 0.2s ease, border-color 0.2s ease; +} + +.tag:hover { + transform: translateY(-2px); + border-color: var(--accent); +} + +/* Items de compétences : glissement vers la droite au survol */ +.skill-list li { + transition: border-color 0.2s ease, color 0.2s ease, transform 0.2s ease; +} + +.skill-list li:hover { + transform: translateX(4px); +} + +/* Liens du footer : légère élévation au survol */ +.footer-links a, +.footer-links .discord-handle { + display: inline-block; + transition: color 0.2s ease, transform 0.2s ease; +} + +.footer-links a:hover, +.footer-links .discord-handle:hover { + transform: translateY(-2px); +} + +/* Effet d'appui sur tous les boutons */ +.btn:active, +.btn-submit:active, +.login-card input[type="submit"]:active, +.contact-form input[type="submit"]:active { + transform: translateY(1px) scale(0.98); +} + +/* On désactive tout si l'utilisateur préfère un affichage sans mouvement */ +@media (prefers-reduced-motion: reduce) { + main, + ::view-transition-old(root), + ::view-transition-new(root) { + animation: none; + } + + .reveal { + opacity: 1; + transform: none; + transition: none; + } + + .site-nav a:hover, + .tag:hover, + .skill-list li:hover, + .footer-links a:hover, + .footer-links .discord-handle:hover { + transform: none; + } +}