From 946e3d3cdf051ea2262e8ab4d376cd994696592a Mon Sep 17 00:00:00 2001 From: Antoine M Date: Tue, 9 Dec 2025 09:32:48 +0100 Subject: [PATCH] FEATURE Big menu refactoring to match dynamlique menu --- header.php | 76 +-------- includes/logos.php | 164 +++++++++++++++++--- resources/css/app.css | 2 + resources/css/components/search-module.css | 128 +++++++++++++++ resources/css/layout/menu-mobile-brand.css | 27 ++++ resources/js/app.ts | 3 + resources/js/search-bar.ts | 164 ++++++++++++++++++++ search.php | 50 ++++++ searchform.php | 41 +++++ template-parts/header/mobile-menu-brand.php | 32 ++++ template-parts/header/primary-menu.php | 53 +++++++ template-parts/header/secondary-menu.php | 21 +++ template-parts/search-module.php | 5 + 13 files changed, 674 insertions(+), 92 deletions(-) create mode 100644 resources/css/components/search-module.css create mode 100644 resources/css/layout/menu-mobile-brand.css create mode 100644 resources/js/search-bar.ts create mode 100644 search.php create mode 100644 searchform.php create mode 100644 template-parts/header/mobile-menu-brand.php create mode 100644 template-parts/header/primary-menu.php create mode 100644 template-parts/header/secondary-menu.php create mode 100644 template-parts/search-module.php diff --git a/header.php b/header.php index 836a145..546be28 100644 --- a/header.php +++ b/header.php @@ -19,81 +19,15 @@
-
-
-
- 'false', - 'theme_location' => 'secondary', - 'li_class' => 'menu-navlink', - 'fallback_cb' => false, - ) - ); ?> - -
+ + + + -
-
- - - - - - -
-
diff --git a/includes/logos.php b/includes/logos.php index 104bafc..5fd4b17 100755 --- a/includes/logos.php +++ b/includes/logos.php @@ -1,34 +1,156 @@ */ - -function cc_mime_types($mimes) +function carhop_allow_svg_upload($mimes) { -$mimes['svg'] = 'image/svg+xml'; -return $mimes; + // Autoriser SVG pour tous les utilisateurs connectés + $mimes['svg'] = 'image/svg+xml'; + $mimes['svgz'] = 'image/svg+xml'; + return $mimes; } -add_filter('upload_mimes', 'cc_mime_types'); +add_filter('upload_mimes', 'carhop_allow_svg_upload', 10, 1); + +// Vérifier les permissions utilisateur pour SVG +function carhop_check_svg_permissions($file) +{ + if ($file['type'] === 'image/svg+xml') { + // Autoriser pour les administrateurs et éditeurs + if (current_user_can('upload_files')) { + return $file; + } else { + $file['error'] = 'Vous n\'avez pas les permissions pour uploader des fichiers SVG.'; + } + } + return $file; +} +add_filter('wp_handle_upload_prefilter', 'carhop_check_svg_permissions'); + +// Debug: afficher les types MIME autorisés +// write_log(get_allowed_mime_types()); + +// Solution alternative : utiliser un hook plus tôt +function carhop_early_svg_support() +{ + add_filter('upload_mimes', function ($mimes) { + $mimes['svg'] = 'image/svg+xml'; + $mimes['svgz'] = 'image/svg+xml'; + return $mimes; + }, 1); +} +add_action('init', 'carhop_early_svg_support', 1); + +// Désactiver la vérification stricte des types MIME pour SVG +function carhop_disable_mime_check($data, $file, $filename, $mimes) +{ + if (pathinfo($filename, PATHINFO_EXTENSION) === 'svg') { + $data['ext'] = 'svg'; + $data['type'] = 'image/svg+xml'; + $data['proper_filename'] = $filename; + } + return $data; +} +add_filter('wp_check_filetype_and_ext', 'carhop_disable_mime_check', 10, 4); -// ############################# -// AJOUT D'UN ESPACE LOGO CUSTOM -// ############################# +/** ------------------------------ + LOGO ALTERNATIF CUSTOMIZER +------------------------------*/ -// function add_logo_customizer_settings($wp_customize) -// { -// $wp_customize->add_setting('logo_semlex_dark'); +/** + * Ajouter l'option de logo alternatif dans le customizer + * (Le logo principal est géré nativement par WordPress dans "Identité du site") + */ +function carhop_customize_register($wp_customize) +{ + // Ajouter le logo alternatif dans la section "Identité du site" existante + $wp_customize->add_setting('carhop_logo_secondary', array( + 'default' => '', + 'sanitize_callback' => 'esc_url_raw', + 'transport' => 'refresh', + )); -// // Add a control to upload the hover logo -// $wp_customize->add_control(new WP_Customize_Image_Control($wp_customize, 'logo_semlex_dark', array( -// 'label' => 'Logo Semlex Sombre', -// 'section' => 'title_tagline', //this is the section where the custom-logo from WordPress is -// 'settings' => 'logo_semlex_dark', -// 'priority' => 8 // show it just below the custom-logo -// ))); -// } + $wp_customize->add_control(new WP_Customize_Image_Control($wp_customize, 'carhop_logo_secondary', array( + 'label' => __('Logo alternatif (Au survol)', 'carhop'), + 'section' => 'title_tagline', // Section native "Identité du site" + 'settings' => 'carhop_logo_secondary', + 'description' => __('Logo alternatif utilisé au survol du logo principal', 'carhop'), + 'priority' => 9, // Juste après le logo principal + ))); +} +add_action('customize_register', 'carhop_customize_register'); -// add_action('customize_register', 'add_logo_customizer_settings'); \ No newline at end of file + +/** ------------------------------ + LOGO SECONDAIRE UTILITIES +------------------------------*/ + +/** + * Récupérer l'URL du logo secondaire + * @return string|false L'URL du logo secondaire ou false si non configuré + */ +function carhop_get_secondary_logo_url() +{ + return get_theme_mod('carhop_logo_secondary', false); +} + +/** + * Récupérer le HTML complet du logo secondaire + * @param string $size Taille de l'image (par défaut 'full') + * @param array $attr Attributs HTML supplémentaires + * @return string|false Le HTML du logo ou false si non configuré + */ +function carhop_get_secondary_logo($size = 'full', $attr = array()) +{ + $secondary_logo_url = get_secondary_logo_url(); + + if (!$secondary_logo_url) { + return false; + } + + // Attributs par défaut + $default_attr = array( + 'alt' => get_bloginfo('name') . ' - Logo alternatif', + 'class' => 'secondary-logo' + ); + + // Fusionner avec les attributs personnalisés + $attr = array_merge($default_attr, $attr); + + // Essayer de récupérer l'ID de l'attachement + $secondary_logo_id = attachment_url_to_postid($secondary_logo_url); + + if ($secondary_logo_id) { + // Utiliser wp_get_attachment_image si on a l'ID + return wp_get_attachment_image($secondary_logo_id, $size, false, $attr); + } else { + // Fallback : créer la balise img manuellement + $attr_string = ''; + foreach ($attr as $key => $value) { + $attr_string .= ' ' . $key . '="' . esc_attr($value) . '"'; + } + return ''; + } +} + +/** + * Afficher le logo secondaire directement + * @param string $size Taille de l'image + * @param array $attr Attributs HTML supplémentaires + */ +function carhop_the_secondary_logo($size = 'full', $attr = array()) +{ + echo get_secondary_logo($size, $attr); +} + +/** + * Vérifier si un logo secondaire est configuré + * @return bool + */ +function carhop_has_secondary_logo() +{ + return !empty(get_secondary_logo_url()); +} diff --git a/resources/css/app.css b/resources/css/app.css index 36a63d8..573594a 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -33,6 +33,7 @@ @import './components/social-networks-links.css'; @import './components/scroll-top.css'; @import './components/member-author-card.css'; +@import './components/search-module.css'; /* ########### EDITOR CONTENT ############ */ @import './editor-content/entry-content.css'; @@ -42,6 +43,7 @@ @import './layout/nav/header.css'; @import './layout/nav/secondary-menu.css'; @import './layout/nav/primary-menu.css'; +@import './layout/menu-mobile-brand.css'; @import './layout/footer.css'; @import './layout/section.css'; diff --git a/resources/css/components/search-module.css b/resources/css/components/search-module.css new file mode 100644 index 0000000..3e2e95c --- /dev/null +++ b/resources/css/components/search-module.css @@ -0,0 +1,128 @@ +.search-module { + @apply w-full absolute + bg-carhop-green-700 + /* bg-white */ + text-white + left-0 + px-16 + py-6 + z-10 + bottom-0 + /* overflow-x-hidden */ + transform translate-y-full; + @apply block overflow-x-hidden; + + animation: translate-in 700ms forwards cubic-bezier(0, 0.51, 0.23, 0.99), + fade-in 600ms forwards ease-out; + + @keyframes translate-in { + from { + transform: translateY(calc(100% - 100px)); + } + to { + transform: translateY(100%); + } + } + + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } + } + @keyframes translate-out { + from { + transform: translateY(100%); + } + to { + transform: translateY(calc(100% - 100px)); + } + } + + &[closed] { + @apply hidden; + } + + &[closing] { + animation: translate-out 800ms forwards ease-in, fade-out 600ms forwards ease-in; + } + + &[opened] { + animation: translate-in 700ms forwards cubic-bezier(0, 0.51, 0.23, 0.99), + fade-in 600ms forwards ease-out; + } + + a { + @apply text-white; + } + input::placeholder { + @apply !text-white; + } + &__wrapper-container { + @apply max-w-screen-xl mx-auto; + } + &__search-form { + @apply flex flex-wrap; + + &__title { + @apply block font-bold text-lg w-full; + } + &__separator { + @apply mt-2 mb-8 bg-neutral-500 border-none opacity-50 w-full; + height: 1px; + } + + &__input { + box-sizing: border-box; + @apply block max-w-full w-full flex-grow !py-4 !border-white px-4 !pl-12 focus-visible:ring-primary focus-visible:ring-2; + @apply border rounded-none; + outline: none !important; + /* border-right: none; + border-top-left-radius: 999px; + border-bottom-left-radius: 999px; + border: 1px solid; */ + + /* box-shadow: 0 0 1px 0px white inset, 0 0 1px 0px white; */ + } + button[type='submit'] { + @apply bg-primary hidden text-white shrink-0 flex justify-center items-center gap-3 rounded-full md:rounded-l-none px-4 py-3 focus-visible:ring-primary focus-visible:ring-2; + max-width: 300px; + outline: none !important; + transform: translateX(-1px); + .search_icon { + @apply invert; + } + } + } + &__searchbar-group { + @apply w-full flex flex-col md:flex-row gap-y-4 relative; + &:before { + @apply content-[''] absolute inset-0 bg-contain bg-center bg-no-repeat w-6 h-6 left-4 top-1/2 -translate-y-1/2; + background-image: url('../resources/img/icons/carhop-rechercher.svg'); + filter: invert(1); + } + } + &__suggestions { + @apply pt-4; + .suggestion-item { + @apply underline underline-offset-4 block w-full mb-2 font-light text-base; + word-break: break-word; + text-decoration-thickness: 1px; + } + } +} + +body:has(.search-module[opened]) main { + filter: blur(2px) brightness(0.8); +} diff --git a/resources/css/layout/menu-mobile-brand.css b/resources/css/layout/menu-mobile-brand.css new file mode 100644 index 0000000..061b824 --- /dev/null +++ b/resources/css/layout/menu-mobile-brand.css @@ -0,0 +1,27 @@ +.menu-mobile-brand { + @apply bg-primary text-white pb-8; + + @screen lg { + @apply hidden; + } + + #mobile-menu-toggle { + @apply underline underline-offset-4; + } + &__inner-elements { + @apply flex justify-between items-center px-6 gap-12; + } + + .website_logo { + @apply shrink; + } + .tools-container { + @apply flex w-fit gap-2; + + .search-button, + .subscribe-button { + @apply w-6 h-6 border-2 flex border-white rounded-full p-2 justify-center items-center shrink-0; + box-sizing: content-box; + } + } +} diff --git a/resources/js/app.ts b/resources/js/app.ts index 9a5a69c..da1e25a 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -3,10 +3,13 @@ import initFooterShapes from './footer'; import handleScrollTop from './utilities/scroll-top'; import handleInsidePageScrolling from './page-scrolling'; import alternatePictures from './alternate-pictures'; +import { searchBarInit } from './search-bar'; + window.addEventListener('load', function () { menuInit(); initFooterShapes(); handleScrollTop(); handleInsidePageScrolling(); alternatePictures(); + searchBarInit(); }); diff --git a/resources/js/search-bar.ts b/resources/js/search-bar.ts new file mode 100644 index 0000000..6e553ca --- /dev/null +++ b/resources/js/search-bar.ts @@ -0,0 +1,164 @@ +interface SearchBarOptions { + searchBarSelector?: string; + buttonSelector?: string; +} + +export default class SearchBar { + private searchBar: HTMLDivElement | null; + private searchButtons: NodeListOf | null; + private searchInput: HTMLInputElement | null; + private isOpen: boolean = false; + private lastScrollY: number = 0; + private activeButton: HTMLButtonElement | null = null; + + constructor(options: SearchBarOptions = {}) { + this.searchBar = document.querySelector('#search-module') as HTMLDivElement; + this.searchButtons = document.querySelectorAll( + '.tools-container .search-button' + ) as NodeListOf; // Il y a 2 boutons de recherche (1 mobile; 1 desktop) + this.searchInput = document.querySelector( + '.search-module__search-form__input' + ) as HTMLInputElement; + + this.init(); + } + + private init(): void { + if (!this.searchBar || !this.searchButtons) return; + + // Initialiser l'état + this.isOpen = this.searchBar.hasAttribute('opened'); + this.lastScrollY = window.scrollY; + this.updateAriaHidden(); + + // Ajouter les event listeners + this.searchButtons.forEach((button) => + button.addEventListener('click', (event) => this.toggle(event)) + ); + this.searchBar.addEventListener('transitionend', this.handleTransitionEnd.bind(this)); + document.addEventListener('keydown', this.handleKeyDown.bind(this)); + document.addEventListener('click', this.handleClickOutside.bind(this)); + window.addEventListener('scroll', this.handleScroll.bind(this)); + } + + private toggle(event: Event): void { + if (!this.searchBar) return; + + // Stocker le bouton qui a été cliqué + this.activeButton = event.currentTarget as HTMLButtonElement; + + this.isOpen = !this.isOpen; + + if (this.isOpen) { + this.open(); + } else { + this.close(); + } + } + + public open(): void { + if (!this.searchBar) return; + + this.searchBar.removeAttribute('closed'); + this.searchBar.setAttribute('opened', ''); + this.isOpen = true; + + // Focus automatique sur le champ de recherche après un petit délai pour laisser l'animation commencer + setTimeout(() => { + if (this.searchInput && this.isOpen) { + this.searchInput.focus(); + } + }, 100); + } + + public close(): void { + if (!this.searchBar) return; + + // Défocus l'input de recherche + if (this.searchInput) { + this.searchInput.blur(); + } + + this.searchBar.setAttribute('closing', ''); + this.searchBar.removeAttribute('opened'); + this.isOpen = false; + + // Écouter la fin de l'animation de fermeture (on attend la plus longue: translate-out 800ms) + const handleAnimationEnd = (event: AnimationEvent) => { + if (event.animationName === 'translate-out') { + this.searchBar?.removeAttribute('closing'); + this.searchBar?.setAttribute('closed', ''); + this.searchBar?.removeEventListener('animationend', handleAnimationEnd); + + // Refocus sur le bouton qui avait ouvert la barre après la fermeture + if (this.activeButton) { + this.activeButton.focus(); + } + } + }; + + this.searchBar.addEventListener('animationend', handleAnimationEnd); + } + + private handleTransitionEnd(): void { + this.updateAriaHidden(); + } + + private handleKeyDown(event: KeyboardEvent): void { + if (event.key === 'Escape' && this.isOpen) { + this.close(); + } + } + + private handleScroll(): void { + if (!this.isOpen) return; + + this.close(); + } + + private handleClickOutside(event: MouseEvent): void { + if (!this.isOpen || !this.searchBar) return; + + const target = event.target as Node; + + // Vérifier si le clic est en dehors de la barre de recherche ET des boutons + const isClickOutsideSearchBar = !this.searchBar.contains(target); + const isClickOnButton = Array.from(this.searchButtons || []).some((button) => + button.contains(target) + ); + + if (isClickOutsideSearchBar && !isClickOnButton) { + this.close(); + } + } + + private updateAriaHidden(): void { + if (!this.searchBar) return; + + if (this.searchBar.hasAttribute('closed') || this.searchBar.hasAttribute('closing')) { + this.searchBar.setAttribute('aria-hidden', 'true'); + } else { + this.searchBar.setAttribute('aria-hidden', 'false'); + } + } + + public isSearchBarOpen(): boolean { + return this.isOpen; + } + + public destroy(): void { + if (!this.searchBar || !this.searchButtons) return; + + // Note: Les event listeners anonymes ne peuvent pas être supprimés facilement + // Dans un vrai projet, il faudrait stocker les références des fonctions + this.searchBar.removeEventListener('transitionend', this.handleTransitionEnd.bind(this)); + document.removeEventListener('keydown', this.handleKeyDown.bind(this)); + document.removeEventListener('click', this.handleClickOutside.bind(this)); + window.removeEventListener('scroll', this.handleScroll.bind(this)); + } +} + +// Fonction de compatibilité pour l'import existant +export function searchBarInit(): SearchBar { + return new SearchBar(); +} diff --git a/search.php b/search.php new file mode 100644 index 0000000..0145cb4 --- /dev/null +++ b/search.php @@ -0,0 +1,50 @@ + + +
+ +
+
+

Rechercher

+

+ + «  » +

+
+
+ + +
+
+

+ found_posts; ?> + résultats +

+
+ + + get_post_type(), + 'post_id' => get_the_ID(), + 'search' => get_search_query(), + )); ?> + + + +
+ 2, + 'prev_text' => __('« Précédent', 'homegrade-theme__texte-fonctionnel'), + 'next_text' => __('Suivant »', 'homegrade-theme__texte-fonctionnel'), + )); + ?> +
+ + +

+ +
+
+ + + + + +
+ + + + + +
+ 1, + 'post_type' => 'revues', + 'post_status' => 'publish', + )); + $lastRevueUrl = get_permalink($lastRevue[0]['ID']); + ?> + + \ No newline at end of file diff --git a/template-parts/header/mobile-menu-brand.php b/template-parts/header/mobile-menu-brand.php new file mode 100644 index 0000000..1b8c9bd --- /dev/null +++ b/template-parts/header/mobile-menu-brand.php @@ -0,0 +1,32 @@ + + \ No newline at end of file diff --git a/template-parts/header/primary-menu.php b/template-parts/header/primary-menu.php new file mode 100644 index 0000000..e901a89 --- /dev/null +++ b/template-parts/header/primary-menu.php @@ -0,0 +1,53 @@ + +
+ +
\ No newline at end of file diff --git a/template-parts/header/secondary-menu.php b/template-parts/header/secondary-menu.php new file mode 100644 index 0000000..8c91001 --- /dev/null +++ b/template-parts/header/secondary-menu.php @@ -0,0 +1,21 @@ + +
+
+ 'false', + 'theme_location' => 'secondary', + 'li_class' => 'menu-navlink', + 'fallback_cb' => false, + ) + ); ?> + +
+ +
\ No newline at end of file diff --git a/template-parts/search-module.php b/template-parts/search-module.php new file mode 100644 index 0000000..a341bc6 --- /dev/null +++ b/template-parts/search-module.php @@ -0,0 +1,5 @@ + \ No newline at end of file