diff --git a/header.php b/header.php
index 836a145..546be28 100644
--- a/header.php
+++ b/header.php
@@ -19,81 +19,15 @@
-
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 @@
+
+
+
+
+
+
+
+
+
+
+ found_posts; ?>
+ résultats
+
+
+
+
+ get_post_type(),
+ 'post_id' => get_the_ID(),
+ 'search' => get_search_query(),
+ )); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 @@
+
+
\ 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