diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..e9761f9 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "portfolio-php", + "runtimeExecutable": "php", + "runtimeArgs": ["-d", "post_max_size=20M", "-d", "upload_max_filesize=20M", "-S", "localhost:8090"], + "port": 8090 + } + ] +} diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..4a65525 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "mcp__Claude_Preview__preview_screenshot", + "mcp__Claude_Preview__preview_start", + "mcp__Claude_Preview__preview_stop", + "Bash(php -l *)", + "mcp__Claude_Preview__preview_console_logs", + "mcp__Claude_Preview__preview_resize" + ] + } +} diff --git a/database/scriptDatabase.sql b/database/scriptDatabase.sql index bc87f45..e718bea 100644 --- a/database/scriptDatabase.sql +++ b/database/scriptDatabase.sql @@ -165,4 +165,18 @@ ADD COLUMN pdf_filename VARCHAR(255) NULL; ALTER TABLE schools ADD COLUMN pdf_data LONGBLOB NULL, -ADD COLUMN pdf_filename VARCHAR(255) NULL; \ No newline at end of file +ADD COLUMN pdf_filename VARCHAR(255) NULL; + +-- ============================================================ +-- Juillet 2026 (5) : icône Devicon en alternative à l'image pour +-- le bandeau de logos (surtout utile pour "Outils que je sais +-- manipuler" : Devicon est un équivalent gratuit de Font Awesome +-- dédié aux logos d'outils/langages/frameworks, cf. devicon.dev) +-- ============================================================ + +-- icon : classes CSS Devicon (ex: "devicon-docker-plain colored"), prioritaire sur logo_url +-- si les deux sont renseignés. logo_url devient facultatif : il faut au moins l'un des deux +-- (validé côté PHP), pour les outils absents de Devicon (Proxmox, GLPI, FOG...) +ALTER TABLE logo_banners +ADD COLUMN icon VARCHAR(100) NULL AFTER name, +MODIFY COLUMN logo_url VARCHAR(255) NULL; \ No newline at end of file diff --git a/elements/header.php b/elements/header.php index 07b3a9b..113350d 100644 --- a/elements/header.php +++ b/elements/header.php @@ -52,6 +52,9 @@ $currentPage = basename($_SERVER['PHP_SELF']); + + diff --git a/functions/listLogos.php b/functions/listLogos.php index dcb763f..8f335ae 100644 --- a/functions/listLogos.php +++ b/functions/listLogos.php @@ -12,11 +12,13 @@ function listeLogos($category = null) { global $dbh; try { + // SELECT * : la colonne icon est optionnelle (le bandeau continue de fonctionner + // avec les logos déjà en place même si la migration SQL n'a pas encore été exécutée) if ($category !== null) { - $stmt = $dbh->prepare('SELECT id, category, name, logo_url, link_url, position FROM logo_banners WHERE category = :category ORDER BY position ASC, id ASC'); + $stmt = $dbh->prepare('SELECT * FROM logo_banners WHERE category = :category ORDER BY position ASC, id ASC'); $stmt->bindValue(':category', $category, PDO::PARAM_STR); } else { - $stmt = $dbh->prepare('SELECT id, category, name, logo_url, link_url, position FROM logo_banners ORDER BY category ASC, position ASC, id ASC'); + $stmt = $dbh->prepare('SELECT * FROM logo_banners ORDER BY category ASC, position ASC, id ASC'); } $stmt->execute(); return $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -30,7 +32,7 @@ function recupererLogoParId($id) { global $dbh; try { - $stmt = $dbh->prepare('SELECT id, category, name, logo_url, link_url, position FROM logo_banners WHERE id = :id'); + $stmt = $dbh->prepare('SELECT * FROM logo_banners WHERE id = :id'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetch(PDO::FETCH_ASSOC); @@ -39,6 +41,20 @@ function recupererLogoParId($id) { } } +// Fonction interne qui affiche le contenu visuel d'une tuile : une icône Devicon si elle est +// renseignée (prioritaire), sinon l'image. Ne retourne rien si aucun des deux n'est présent. +function afficherLogoVisuel($logo) { + $icone = trim($logo['icon'] ?? ''); + $image = trim($logo['logo_url'] ?? ''); + $nom = htmlspecialchars($logo['name']); + + if ($icone !== '') { + echo ''; + } elseif ($image !== '') { + echo '' . $nom . ''; + } +} + // Fonction qui affiche le bandeau défilant d'une catégorie de logos sur la page publique // N'affiche rien si la catégorie est vide (la section entière est masquée côté page d'accueil) function afficherLogos($category) { @@ -58,7 +74,6 @@ function afficherLogos($category) { for ($repetition = 0; $repetition < 2; $repetition++) { foreach ($logos as $logo) { $nom = htmlspecialchars($logo['name']); - $image = htmlspecialchars($logo['logo_url']); $lien = trim($logo['link_url'] ?? ''); if ($lien !== '') { @@ -67,7 +82,7 @@ function afficherLogos($category) { echo '
'; } - echo '' . $nom . ''; + afficherLogoVisuel($logo); echo $lien !== '' ? '' : '
'; } } @@ -91,11 +106,12 @@ function afficherLogosAdmin() { foreach ($logos as $logo) { $id = (int) $logo['id']; $nom = htmlspecialchars($logo['name']); - $image = htmlspecialchars($logo['logo_url']); $badge = $logo['category'] === 'partner' ? 'Entreprise' : 'Outil'; echo '
'; - echo '
' . $nom . '
'; + echo '
'; + afficherLogoVisuel($logo); + echo '
'; echo '
'; echo '' . $badge . ''; echo '

' . $nom . '

'; diff --git a/functions/listSkills.php b/functions/listSkills.php index 02f2991..7b0562b 100644 --- a/functions/listSkills.php +++ b/functions/listSkills.php @@ -32,6 +32,41 @@ function recupererSkillParId($id) { } } +// Icônes Devicon dont la couleur de marque "colored" est noire ou quasi-noire (vérifié dans +// devicon.min.css : linux #000, apple #000, bash #293138, ansible #1a1918) : invisibles sur +// la tuile sombre du bandeau. On les affiche dans la couleur claire par défaut à la place. +const DEVICON_COLORED_TROP_SOMBRE = ['devicon-linux-plain', 'devicon-apple-plain', 'devicon-bash-plain', 'devicon-ansible-plain']; + +// Fonction interne qui affiche le visuel (icône ou image) d'un item de compétence. +// Format accepté avant le "|" : soit des classes d'icône (Font Awesome ou Devicon), soit +// "img:URL" pour utiliser une vraie image de logo (pour les outils absents des polices +// d'icônes, ex : GLPI, FOG, Proxmox, VirtualBox, Guacamole). +// $colorise active les vraies couleurs de marque des icônes Devicon (utilisé uniquement +// pour le bandeau de la home : sur la page Compétences les icônes restent en orange +// uniforme, cohérent avec le reste de la liste) +function afficherVisuelItem($itemIcone, $classe = '', $colorise = false) { + if (strpos($itemIcone, 'img:') === 0) { + $image = htmlspecialchars(substr($itemIcone, 4)); + echo ''; + return; + } + + if ($colorise && strpos($itemIcone, 'devicon-') === 0 && strpos($itemIcone, 'colored') === false) { + $estTropSombre = false; + foreach (DEVICON_COLORED_TROP_SOMBRE as $slug) { + if (strpos($itemIcone, $slug) === 0) { + $estTropSombre = true; + break; + } + } + if (!$estTropSombre) { + $itemIcone .= ' colored'; + } + } + + echo ''; +} + // 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) { @@ -53,7 +88,9 @@ function afficherSkillCard($skill, $admin = false) { if (strpos($item, '|') !== false) { list($itemIcone, $label) = array_map('trim', explode('|', $item, 2)); } - echo '
  • ' . htmlspecialchars($label) . '
  • '; + echo '
  • '; + afficherVisuelItem($itemIcone, 'skill-item-visual'); + echo ' ' . htmlspecialchars($label) . '
  • '; } echo ''; @@ -96,3 +133,80 @@ function afficherSkillsAdmin() { } echo '
    '; } + +// Items exclus du bandeau de la home au cas par cas, en plus du filtre automatique +// (icône Devicon "réelle" mais jugée pas assez reconnaissable seule, sans le texte à côté) +const OUTILS_EXCLUS_DU_BANDEAU = ['Bash']; + +// Fonction qui récupère tous les items de compétences représentant un vrai outil identifiable +// (icône Devicon ou image "img:"), toutes catégories confondues. Les items en texte seul +// SONT EXCLUS, ainsi que les icônes Font Awesome génériques (fa-solid/fa-brands) : dans un +// bandeau qui n'affiche pas le texte à côté (juste une info-bulle au survol), une icône +// générique comme un pictogramme "carte" pour "Adressage IP" ou "carnet de contact" pour +// "LDAP" ne représente aucun outil reconnaissable — ça reste illisible pour un visiteur qui +// ne survole pas. Ces items restent affichés normalement sur la page Compétences (avec leur +// texte à côté, donc sans ambiguïté), seul le bandeau de la home les filtre. +function listeIconesCompetences() { + $categories = listeSkills(); + $resultats = []; + + foreach ($categories as $categorie) { + $items = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $categorie['items']))); + + foreach ($items as $item) { + if (strpos($item, '|') === false) { + continue; + } + + list($icone, $label) = array_map('trim', explode('|', $item, 2)); + if ($icone === '' || $label === '') { + continue; + } + + // Seuls les vrais logos (Devicon ou image) sont assez reconnaissables sans texte + $estUnVraiLogo = strpos($icone, 'devicon-') === 0 || strpos($icone, 'img:') === 0; + if (!$estUnVraiLogo) { + continue; + } + + if (in_array($label, OUTILS_EXCLUS_DU_BANDEAU, true)) { + continue; + } + + $resultats[] = ['icon' => $icone, 'label' => $label]; + } + } + + return $resultats; +} + +// Fonction qui affiche le bandeau défilant des outils, directement à partir des icônes +// de la page Compétences (voir listeIconesCompetences). Réutilise les mêmes classes CSS +// que les bandeaux de logo_banners pour un rendu visuel identique (voir primary_css.css). +// "logo-marquee-tools" désactive le filtre grayscale/survol : contrairement aux logos +// d'entreprises, on veut que les vrais outils soient reconnaissables immédiatement. +function afficherOutilsCompetences() { + $outils = listeIconesCompetences(); + + if (empty($outils)) { + return; + } + + echo '
    '; + echo '
    '; + + // On répète deux fois la liste : le défilement CSS boucle sur -50%, ce qui donne + // un effet de scroll infini fluide et sans coupure + for ($repetition = 0; $repetition < 2; $repetition++) { + foreach ($outils as $outil) { + $label = htmlspecialchars($outil['label']); + + echo '
    '; + afficherVisuelItem($outil['icon'], '', true); + echo '
    '; + } + } + + echo '
    '; + echo '
    '; +} diff --git a/pages/admin.php b/pages/admin.php index ffa7c85..c19a5ac 100644 --- a/pages/admin.php +++ b/pages/admin.php @@ -319,10 +319,15 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
    -

    Logos (page d'accueil)

    +

    Logos partenaires (page d'accueil)

    +

    + Le bandeau "Outils que je sais manipuler" est désormais dérivé automatiquement de la + page Compétences (icône dans le champ "icone|texte" de chaque item). + Cette section ne gère plus que le bandeau "Ils m'ont fait confiance". +

    > - +
    @@ -332,17 +337,20 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1'; - - + + - - + + - - + + + + + diff --git a/pages/home.php b/pages/home.php index e369d95..94ef06e 100644 --- a/pages/home.php +++ b/pages/home.php @@ -12,10 +12,13 @@ require_once('./../elements/header.php'); // La notification est maintenant dynamique : modifiable depuis le panel Admin (table settings) require_once('../functions/listSettings.php'); -// Bandeaux de logos ("Outils que je sais manipuler" / "Ils m'ont fait confiance"), -// gérés depuis le panel Admin (table logo_banners) +// Bandeau "Ils m'ont fait confiance", géré depuis le panel Admin (table logo_banners) require_once('../functions/listLogos.php'); +// Bandeau "Outils que je sais manipuler" : dérivé directement des icônes de la page +// Compétences, pas de saisie séparée (voir listeIconesCompetences) +require_once('../functions/listSkills.php'); + // Affichage de la notification (si elle est activée dans le panel Admin) afficherNotice(); ?> @@ -39,13 +42,13 @@ afficherNotice();
    - - + +

    Outils que je sais manipuler

    - +
    diff --git a/scripts/addLogo.php b/scripts/addLogo.php index 4e1b519..b14cc3f 100644 --- a/scripts/addLogo.php +++ b/scripts/addLogo.php @@ -16,21 +16,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // La catégorie est restreinte à 'tool' ou 'partner' (sinon on retombe sur 'tool') $category = ($_POST['category'] ?? '') === 'partner' ? 'partner' : 'tool'; $name = trim($_POST['name'] ?? ''); + $icon = trim($_POST['icon'] ?? ''); $logo_url = trim($_POST['logourl'] ?? ''); $link_url = trim($_POST['linkurl'] ?? ''); $position = (int) ($_POST['position'] ?? 0); - // On vérifie que les champs obligatoires ne sont pas vide - if (!empty($name) && !empty($logo_url)) { + // On vérifie que les champs obligatoires ne sont pas vides : il faut un nom, et + // au moins l'un des deux visuels (icône Devicon ou image) + if (!empty($name) && (!empty($icon) || !empty($logo_url))) { try { // On prépare la requête SQL pour insérer le nouveau logo - $stmt = $dbh->prepare('INSERT INTO logo_banners (category, name, logo_url, link_url, position) VALUES (:category, :name, :logo_url, :link_url, :position)'); + $stmt = $dbh->prepare('INSERT INTO logo_banners (category, name, icon, logo_url, link_url, position) VALUES (:category, :name, :icon, :logo_url, :link_url, :position)'); // On execute la requete préparée $stmt->execute([ ':category' => $category, ':name' => $name, - ':logo_url' => $logo_url, + ':icon' => $icon !== '' ? $icon : null, + ':logo_url' => $logo_url !== '' ? $logo_url : null, ':link_url' => $link_url, ':position' => $position ]); @@ -48,7 +51,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { exit; } } else { - echo "Veuillez remplir tous les champs obligatoires."; + echo "Veuillez remplir le nom et au moins une icône ou une image."; exit; } } else { diff --git a/scripts/updateLogo.php b/scripts/updateLogo.php index 4a43d94..43a8d6a 100644 --- a/scripts/updateLogo.php +++ b/scripts/updateLogo.php @@ -15,7 +15,7 @@ if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') { if ( // Si les champs sont vides on renvoie vers admin.php !isset($_POST['id']) || !is_numeric($_POST['id']) || empty($_POST['name']) || - empty($_POST['logourl']) + (empty($_POST['icon']) && empty($_POST['logourl'])) ) { header('Location: ../pages/admin.php'); exit; @@ -25,19 +25,21 @@ if ( // Si les champs sont vides on renvoie vers admin.php $id = (int) $_POST['id']; $category = ($_POST['category'] ?? '') === 'partner' ? 'partner' : 'tool'; $name = trim($_POST['name']); -$logourl = trim($_POST['logourl']); +$icon = trim($_POST['icon'] ?? ''); +$logourl = trim($_POST['logourl'] ?? ''); $linkurl = trim($_POST['linkurl'] ?? ''); $position = (int) ($_POST['position'] ?? 0); try { // On essaie d'executer une requête qui est préparée $stmt = $dbh->prepare(' UPDATE logo_banners - SET category = :category, name = :name, logo_url = :logo_url, link_url = :link_url, position = :position + SET category = :category, name = :name, icon = :icon, logo_url = :logo_url, link_url = :link_url, position = :position WHERE id = :id '); $stmt->bindValue(':category', $category, PDO::PARAM_STR); $stmt->bindValue(':name', $name, PDO::PARAM_STR); - $stmt->bindValue(':logo_url', $logourl, PDO::PARAM_STR); + $stmt->bindValue(':icon', $icon !== '' ? $icon : null, $icon !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); + $stmt->bindValue(':logo_url', $logourl !== '' ? $logourl : null, $logourl !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':link_url', $linkurl, PDO::PARAM_STR); $stmt->bindValue(':position', $position, PDO::PARAM_INT); $stmt->bindValue(':id', $id, PDO::PARAM_INT); diff --git a/styles/primary_css.css b/styles/primary_css.css index 438910c..264e18b 100644 --- a/styles/primary_css.css +++ b/styles/primary_css.css @@ -1349,8 +1349,10 @@ img { } } -/* Pas de fond/bordure : les logos flottent directement sur le fond de la page, - comme un bandeau "trusted by" classique */ +/* Carte derrière chaque logo, avec un fond SOMBRE FIXE (pas var(--surface), qui devient + blanc en thème clair) : certains logos PNG ont un fond noir non transparent intégré à + l'image, qui doit rester sur un fond sombre constant pour ne pas ressortir moche + (carré noir visible) quel que soit le thème actif sur le reste du site */ .logo-tile { flex-shrink: 0; display: flex; @@ -1359,17 +1361,21 @@ img { width: 160px; height: 90px; padding: 18px 24px; - background: transparent; - border: none; + background: #14141c; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--radius); text-decoration: none; - transition: transform 0.2s ease; + transition: transform 0.2s ease, border-color 0.2s ease, background-color 0.2s ease; } a.logo-tile:hover { transform: translateY(-3px); + border-color: var(--accent-border); + background: #1c1c26; } -.logo-tile img { +.logo-tile img, +.logo-tile i { max-width: 100%; max-height: 100%; object-fit: contain; @@ -1377,17 +1383,36 @@ a.logo-tile:hover { transition: filter 0.25s ease; } -.logo-tile:hover img { +/* Taille de la glyphe pour les icônes Devicon (les images gardent leur taille naturelle) */ +/* Couleur fixe (pas var(--text), qui devient sombre en thème clair et rendrait + l'icône invisible sur le fond de tuile qui lui reste volontairement sombre) */ +.logo-tile i { + font-size: 44px; + line-height: 1; + color: #e8e8f2; +} + +.logo-tile:hover img, +.logo-tile:hover i { filter: grayscale(0) opacity(1); } /* Sur mobile le survol n'existe pas : on affiche les logos en couleur directement */ @media (hover: none) { - .logo-tile img { + .logo-tile img, + .logo-tile i { filter: grayscale(0) opacity(0.85); } } +/* Bandeau "Outils" : pas de niveaux de gris, on veut que les vrais logos des outils + soient reconnaissables immédiatement, sans avoir à survoler chaque tuile */ +.logo-marquee-tools .logo-tile img, +.logo-marquee-tools .logo-tile i { + filter: none; + opacity: 1; +} + @media (max-width: 620px) { .logo-tile { width: 130px; @@ -1446,6 +1471,11 @@ a.logo-tile:hover { object-fit: contain; } +.logo-admin-preview i { + font-size: 40px; + color: var(--text); +} + .logo-admin-info { padding: 14px 16px 4px; } @@ -1564,6 +1594,14 @@ a.logo-tile:hover { text-align: center; } +/* Vraie image de logo (format "img:URL") pour les outils absents des polices d'icônes */ +.skill-item-visual { + width: 18px; + height: 18px; + object-fit: contain; + flex-shrink: 0; +} + /* Boutons admin dans les cartes de compétences */ .skill-category .admin-actions { margin-top: 18px; @@ -1955,6 +1993,18 @@ a.card-link::after { font-size: 0.85em; } +/* Texte d'introduction sous un titre de section admin (voir Logos partenaires) */ +.admin-section-intro { + max-width: 1200px; + margin: 0 auto 16px; + padding: 0 clamp(16px, 4vw, 40px); +} + +.admin-section-intro a { + color: var(--accent-light); + font-weight: 600; +} + /* Bloc repliable contenant chaque formulaire */ .admin-block { max-width: 1200px;