->set_width(50)
->set_required(),
+ Field::make('text', 'thumbnail_caption', __('Description courte'))
+ ->set_help_text("Par exemple : « Hôtel, Lyon ». Affiché sous la vignette sur la page d'index")
+ ->set_required(),
+
Field::make('rich_text', 'description', __('Description', 'usines'))
->set_required(),
Field::make('text', 'testimonial_author', __('Auteur du témoignage', 'usines')),
]);
+
+ // Add special options page for extra content on Réalisations index page
+ Container::make('theme_options', __("Réalisations : options de la page d'index"))
+ ->set_page_menu_title("Page d'index")
+ ->set_page_parent('edit.php?post_type=realisation') // Add to the "Réalisations" CPT menu
+ ->add_fields([
+
+ Field::make('text', 'realisations_title', __("Titre de la page d'index des réalisations"))
+ ->set_default_value('Réalisations'),
+
+ Field::make('textarea', 'realisations_intro_text', __("Texte d'introduction"))
+ ->set_help_text('Contenu affiché au début de la page <a href="/realisations" target="_blank">Réalisations</a>.')
+ ]);
});
}
// Register Carbon Fields
add_action('after_setup_theme', function () {
- if (isset($_GET['action']) && $_GET['action'] !== 'elementor') {
+ if (!(is_admin() && isset($_GET['action']) && $_GET['action'] === 'elementor')) {
Carbon_Fields::boot();
}
});
$elementor->widgets_manager->register_widget_type( new Widgets\Circle() );
$elementor->widgets_manager->register_widget_type( new Widgets\ImageMap() );
$elementor->widgets_manager->register_widget_type( new Widgets\TeamGrid() );
+ $elementor->widgets_manager->register_widget_type( new Widgets\Realisations() );
}
--- /dev/null
+<?php
+
+namespace Cube\Elementor\Widgets;
+
+use Elementor\Controls_Manager;
+
+use function Roots\view;
+
+
+class Realisations extends _Base {
+
+ protected $_has_template_content = false; // Tell Elementor that content is all rendered dynamically
+
+ // Widget name / ID
+ public function get_name() {
+ return 'cube-realisations';
+ }
+
+ // Elementor widget title
+ public function get_title() {
+ return __( 'Réalisations', 'cube' );
+ }
+
+ // Elementor interface icon
+ public function get_icon() {
+ return 'eicon-post-list';
+ }
+
+ /**
+ * List of scripts the widget depends on.
+ * Used to set scripts dependencies required to run the widget.
+ *
+ * @since 1.0.0
+ * @access public
+ * @return array Widget scripts dependencies.
+ */
+ public function get_script_depends() {
+ return [];
+ }
+ /**
+ * Register the widget controls.
+ * Adds different input fields to allow the user to change and customize the widget settings.
+ *
+ * @since 1.0.0
+ * @access protected
+ */
+ protected function _register_controls() {
+
+ $this->start_controls_section(
+ 'section_content',
+ [
+ 'label' => __( 'Réalisations', 'cube' ),
+ ]
+ );
+
+ $this->add_control(
+ 'widget_description',
+ [
+ 'raw' => __( 'This widget will display the most recent réalisation posts.', 'cube' ),
+ 'type' => Controls_Manager::RAW_HTML,
+ 'content_classes' => 'elementor-descriptor',
+ ]
+ );
+
+ $this->add_control(
+ 'posts_limit',
+ [
+ 'label' => __( 'Number of posts to display', 'cube' ),
+ 'type' => Controls_Manager::NUMBER,
+ 'default' => 10,
+ ]
+ );
+
+ $this->end_controls_section();
+
+ $this->common_controls();
+ }
+ /**
+ * Render the widget output on the frontend.
+ * Written in PHP and used to generate the final HTML.
+ *
+ * @since 1.0.0
+ * @access protected
+ */
+ protected function render() {
+
+ $posts_limit = $this->get_settings('posts_limit');
+
+ $realisation_posts = wp_get_recent_posts([
+ 'numberposts' => $posts_limit,
+ 'orderby' => 'post_date',
+ 'order' => 'DESC',
+ 'post_type' => 'realisation',
+ 'post_status' => 'publish',
+ 'suppress_filters' => true
+ ]);
+
+ if ($realisation_posts) {
+ foreach ($realisation_posts as $realisation_post) {
+ echo view('partials.content-realisation', compact('realisation_post'));
+ }
+ }
+
+ }
+}
--- /dev/null
+<?php
+
+namespace App\View\Composers;
+
+use Roots\Acorn\View\Composer;
+
+class Realisation extends Composer
+{
+ /**
+ * List of views served by this composer.
+ *
+ * @var array
+ */
+ protected static $views = [
+ 'partials.content-single-realisation',
+ ];
+
+ /**
+ * Data to be passed to view before rendering.
+ *
+ * @return array
+ */
+ public function with()
+ {
+ $postID = get_the_ID();
+
+ return [
+ 'category' => $this->get_category(),
+ 'hero_image' => carbon_get_post_meta($postID, 'hero_image'),
+ 'description' => carbon_get_post_meta($postID, 'description'),
+ 'gallery' => carbon_get_post_meta($postID, 'gallery'),
+ 'testimonial' => carbon_get_post_meta($postID, 'testimonial'),
+ 'testimonial_author' => carbon_get_post_meta($postID, 'testimonial_author'),
+ ];
+ }
+
+ /**
+ * Returns the category name (there can only be one)
+ *
+ * @return string
+ */
+ public function get_category()
+ {
+ // Since there can only be one category assigned, we can flatten the array
+ // out and return the category if it exists (ref: https://stackoverflow.com/a/8131148)
+ return reset(get_the_terms(get_the_ID(), 'realisation_category'));
+ }
+}
--- /dev/null
+<?php
+
+namespace App\View\Composers;
+
+use Roots\Acorn\View\Composer;
+use Elementor\Plugin as ElementorPlugin;
+
+use function Roots\asset;
+
+class Realisations extends Composer
+{
+ /**
+ * List of views served by this composer.
+ *
+ * @var array
+ */
+ protected static $views = [
+ 'archive-realisation',
+ ];
+
+ /**
+ * Data to be passed to view before rendering.
+ *
+ * @return array
+ */
+ public function with()
+ {
+ wp_enqueue_script('masonry-columns', asset('scripts/masonry-columns.js'), [], null, true);
+
+ return [
+ 'title' => carbon_get_theme_option('realisations_title'),
+ 'intro' => carbon_get_theme_option('realisations_intro_text'),
+ 'footer' => $this->get_footer(),
+ ];
+ }
+
+ /**
+ * Returns the footer section from the Elementor template library.
+ *
+ * @return string
+ */
+ public function get_footer()
+ {
+ // Fetch content from Elementor template named 'realisations-footer'
+ $page = get_page_by_path('realisations-footer', OBJECT, 'elementor_library');
+ $content = '';
+
+ if ($page->ID) {
+ $content = ElementorPlugin::instance()->frontend->get_builder_content_for_display($page->ID);
+ }
+
+ return $content;
+ }
+}
add_filter('excerpt_more', function () {
return ' … <a href="' . get_permalink() . '">' . __('Continued', 'sage') . '</a>';
});
+
+/**
+ * When calling wp_list_categories(), WordPress doesn't apply the .current-cat class to the "Show All" categories link.
+ * It is also missing the .cat-item class, which makes styling more awkward, so both these need to be added...
+ * Ref: https://wordpress.stackexchange.com/a/331859
+ */
+add_filter('wp_list_categories', function ( $output, $args ) {
+ if ( array_key_exists( 'show_option_all', $args ) && $args['show_option_all'] ) {
+ if ( ! array_key_exists( 'current_category', $args ) || $args['current_category'] ) {
+ if ( is_category() || is_tax() || is_tag() ) {
+ if ( ! array_key_exists( 'taxonomy', $args ) ) {
+ $args['taxonomy'] = 'category';
+ }
+ $current_term_object = get_queried_object();
+ if ( $args['taxonomy'] !== $current_term_object->taxonomy ) {
+ $output = str_replace( "class='cat-item-all'", "class='cat-item-all current-cat'", $output );
+ }
+ } else {
+ $output = str_replace( "class='cat-item-all'", "class='cat-item-all current-cat'", $output );
+ }
+ }
+ }
+
+ // Add missing 'cat-item' class before cat-item-all
+ $output = str_replace( "class='cat-item-all", "class='cat-item cat-item-all", $output );
+
+ return $output;
+}, 10, 2 );
use function Roots\asset;
+/**
+ * Override Elementor CSS order
+ * Normally, the Sage CSS is the last to load but we need Elementor's post specific CSS
+ * to load after the theme so it can override things like the padding. Elementor's default
+ * frontend CSS contains 10px padding on all populated elements, which messes with our
+ * designs so we override this in our theme. However, to override it, we are forced to use a
+ * CSS selector with high specificity in our theme's CSS:
+ * .elementor-column-gap-default > .elementor-row > .elementor-column > .elementor-element-populated
+ * When Elementor generates its post-xx.css files for custom styling set on a page, the specificity
+ * of this CSS rule is the same as ours so it can never override ours unless it is the last stylesheet
+ * to load. The desired loading order is:
+ * 1) Elementor Global / Frontend CSS
+ * 2) Sage Main CSS (Theme CSS)
+ * 3) Elementor post specific CSS (post-xx.css)
+ *
+ * Elementor has a hook when registering CSS files - https://code.elementor.com/hooks/elementor-css-file-name-enqueue/
+ * However, I couldn't get this to work properly with add_action and we need to use the post ID (get_the_ID()), which
+ * doesn't always seem to be available. I need to figure out where this hook should be added so that it is ready...
+ *
+ * For now, the solution is quite low-level and we inject a dependency of our theme's 'sage/app' CSS file into
+ * Elementor's post specific CSS file just before the stylesheets are printed. By having our theme as a dependency,
+ * Elementor's post-xx.css file is forced to load after the theme's CSS...
+ *
+ * TODO: revisit this and see if there's a better solution. See issue here: https://github.com/elementor/elementor/issues/7658
+ */
+add_action('wp_print_styles', function() {
+ global $wp_styles;
+ $sage_css_handle = 'sage/app';
+ $elementor_css_handle = 'elementor-post-'. get_the_ID();
+
+ // First, check if Elementor styles are loaded yet and if not, load them because we need to be able
+ // to make them a dependency of the main theme styles in order to have the correct CSS order...
+ // If we don't do this, on some pages that load Elementor late (eg. index.blade.php), the Elementor CSS
+ // will be loaded when fetching the builder content but it will be in the footer due to the late call.
+ // Although not perfect, it's easiest to always load the Elementor frontend CSS even if it's not strictly needed.
+ if (!isset($wp_styles->registered['elementor-frontend'])) {
+ \Elementor\Plugin::instance()->frontend->enqueue_styles();
+ }
+
+ // Add the theme's main CSS as a dependency for the post-specific CSS in order to get the
+ // theme CSS to output first, thereby allowing it to be overridden by the post specific CSS.
+ if (isset($wp_styles->registered[$elementor_css_handle])) {
+ $wp_styles->registered[$elementor_css_handle]->deps[] = $sage_css_handle;
+ }
+
+});
+
+
/**
* Register the theme assets.
*
wp_enqueue_script('comment-reply');
}
- wp_enqueue_style('sage/app.css', asset('styles/app.css')->uri(), false, null);
+ // Ensure Elementor CSS is loaded before theme CSS (see above)
+ $styles = ['styles/app.css'];
+
+ foreach ($styles as $stylesheet) {
+ if (asset($stylesheet)->exists()) {
+ wp_enqueue_style('sage/' . basename($stylesheet, '.css'), asset($stylesheet)->uri(), ['elementor-frontend'], null);
+ }
+ }
+
}, 100);
/**
'labels' => [
'singular' => 'Catégorie',
'plural' => 'Catégories',
+ 'slug' => 'categories-realisation',
],
],
],
"babel-loader": "^8.2.1",
"browser-sync": "^2.26.12",
"browser-sync-webpack-plugin": "^2.0.1",
+ "columns.js": "^1.1.2",
"cross-env": "^7.0.2",
"eslint": "^7.7.0",
"eslint-plugin-import": "^2.22.0",
--- /dev/null
+//-- Lightweight Masonry Library Setup
+import Masonry from 'columns.js'; // https://github.com/mladenilic/columns.js
+
+const debounce = (callback, wait) => {
+ let timeout;
+
+ return () => {
+ const context = this, args = arguments;
+ clearTimeout(timeout);
+ timeout = setTimeout(() => {
+ timeout = null;
+ callback.apply(context, args);
+ }, wait);
+ };
+};
+
+// Auto-trigger for any elements carrying the data-masonry-columns attribute
+// Settings come from JSON in the data attribute of the element to make this more flexible
+document.querySelectorAll('[data-masonry-columns]')
+ .forEach(function(grid) {
+ let masonry = new Masonry(grid, JSON.parse(grid.dataset.masonryColumns));
+
+ // Update masonry columns when resizing
+ window.addEventListener('resize', debounce(() => {
+ masonry.render();
+ }, 50));
+ });
+
@import 'common/layout'
@import 'components/*'
@import 'widgets/*'
-//@import 'pages/*'
+@import 'pages/*'
// Allow spacing classes to override others defined here
@import 'common/spacing'
a
@apply transition-colors unquote('hover:text-red')
+b, strong
+ font-weight: bold
+
img
.rounded-full & // So this can be applied to parent element and also to override Elementor default CSS
border-radius: 999px
--- /dev/null
+// Columns.js Masonry
+$masonry-max-columns = 3 // Max number of columns to support
+$masonry-column-gap = 5.88% // Desired gap between columns (based on container width)
+
+[data-columns]
+ display: flex
+ justify-content: space-between
+
+for columns in range(1, $masonry-max-columns)
+ [data-columns=\"{columns}\"] > *
+ // Calculate how wide the columns should be based on number of columns
+ // and also subtract enough to leave the correct column gaps.
+ // There will be (columns - 1) gaps so the amount to reduce each column by is gaps / columns * gap-size
+ // Flexbox's justify-content: space-between will make the gaps even with from leftover space
+ //flex-basis: (100% / columns - ($masonry-column-gap * (columns - 1) / columns))
+ flex-basis: s('calc(100% / %d - (%s * %d / %d))', columns, $masonry-column-gap, columns - 1, columns)
+ //flex-basis: s('calc(100% / %d)', columns)
--- /dev/null
+.realisations
+ &-categories
+ margin-top: -1rem // Offset margin for internal row spacing when list wraps
+
+ li
+ display: inline-block
+ margin-top: 1rem
+
+ &:not(:last-child) // Spacing between items
+ margin-right: 2.5rem
+
+ &.current-cat a:after // Rotate arrow down when current category
+ transform: rotate(45deg)
+ transform-origin: 75% 75%
+
+ a
+ position: relative
+
+ // Circle arrow icon (>)
+ &:before // Circle ( )
+ content: ''
+ display: inline-block
+ width: 2em
+ height: @width
+ vertical-align: middle
+ margin-right: 0.5em
+ border-radius: 50%
+ border: 2px solid
+
+ &:after // Arrow >
+ content: ''
+ width: 0.5625em
+ height: @width
+ border-style: solid
+ border-width: 0 2px 2px 0
+ transform: rotate(-45deg)
+ position: absolute
+ left: 0.6em
+ top: 0.4em
+
+
+{{-- Réalisations Index Page --}}
+{{-- See app/View/Composers/Realisations.php for data sources --}}
@extends('layouts.app')
@section('content')
-
- <div class="container-content py-2v px-1v">
-
- {{-- TODO !
-
- @if (! have_posts())
- @alert(['type' => 'warning'])
- {{ __('Sorry, no results were found.', 'sage') }}
- @endalert
-
- {!! get_search_form(false) !!}
- @endif
-
- @while (have_posts()) @php(the_post())
- @includeFirst(['partials.content-'.get_post_type(), 'partials.content'])
- @endwhile
+ @php
+ $settings = [
+ 'columns' => 3,
+ 'algorithm' => 'chronological',
+ 'breakpoints' => [ // min-width => number of columns
+ 1100 => 3,
+ 516 => 2,
+ 0 => 1,
+ ],
+ ];
+
+ $masonry = json_encode($settings);
+ @endphp
+
+
+ <div class="py-2v px-1v">
+ <h1 class="text-2xl text-center mb-2v">{{ $title }}</h1>
+
+ <div class="realisations-wrapper relative mx-auto py-1v" style="max-width: 1632px">
+ {{-- Inset background layer --}}
+ <div class="bg-light absolute inset-0 mx-1v sm:-mx-1v" style="z-index: -1"></div>
+
+ {{-- Intro text that is stored in theme's custom options (see Réalisations menu in dashboard) --}}
+ <div class="text-center px-2v sm:px-1v mx-auto mb-1v" style="max-width: 1226px">{{ $intro }}</div>
+
+ {{-- Réalisation Categories --}}
+ <ul class="realisations-categories font-medium text-center mx-2v sm:mx-1v sm:flex sm:flex-col sm:text-left">
+ {!!
+ wp_list_categories([
+ 'echo' => 0,
+ 'show_option_all' => __('Tout voir'), // See app/filters.php for extra treatment to add current class
+ 'style' => 'list',
+ 'taxonomy' => 'realisation_category',
+ 'title_li' => '',
+ ]) !!}
+ </ul>
+
+ <div class="-mt-1v" data-masonry-columns="{{ $masonry }}">
+ @while (have_posts()) @php(the_post())
+ @includeFirst(['partials.content-'.get_post_type(), 'partials.content'])
+ @endwhile
+ </div>
{!! get_the_posts_navigation() !!}
- --}}
-
+ </div>
</div>
+
+ {!! $footer !!}
+
@endsection
@section('sidebar')
--- /dev/null
+@php
+ // Note: since this template is used by the standard WordPress loop AND by the Elementor widget, which
+ // exists outside the loop, we are sometimes getting data that is passed in and other times relying on
+ // the global loop context. As a result, the code had to be modified a bit to work in both situations.
+ $postID = isset($realisation_post) ? $realisation_post['ID'] : $post->ID;
+ $thumbnail = carbon_get_post_meta($postID, 'thumbnail');
+ $thumbnail_image = $thumbnail ? wp_get_attachment_image($thumbnail, 'large') : '';
+ $thumbnail_caption = carbon_get_post_meta($postID, 'thumbnail_caption');
+@endphp
+
+<article <?php post_class('mt-2v sm:mt-12', $postID) ?>>
+ <a href="{{ get_permalink($postID) }}">{!! $thumbnail_image !!}</a>
+ <div class="text-center mt-6">{{ $thumbnail_caption }}</div>
+</article>
--- /dev/null
+<article @php(post_class())>
+ <header class="text-center mb-2v">
+ <h1 class="bg-light p-1v text-xl inline-block rounded-lg">
+ {!! $title !!}
+ <br>
+ <span class="text-lg opacity-50">ID: {{ get_post_field('post_name') }}</span>
+ </h1>
+ {{-- @include('partials/entry-meta')--}}
+ </header>
+
+ <div class="entry-content">
+ @php(the_content())
+ </div>
+
+ {{--
+ <footer>
+ {!! wp_link_pages(['echo' => 0, 'before' => '<nav class="page-nav"><p>' . __('Pages:', 'sage'), 'after' => '</p></nav>']) !!}
+ </footer>
+
+ @php(comments_template())
+ --}}
+</article>
--- /dev/null
+{{-- Réalisation Detail Page --}}
+{{-- See app/View/Composers/Realisation.php for data sources --}}
+<article @php(post_class())>
+ <header class="text-center mb-8">
+ <h1 class="text-2xl mb-8">{!! $title !!}</h1>
+ {{-- @include('partials/entry-meta')--}}
+ <div class="realisation-category">{!! $category->name !!}</div>
+ </header>
+
+ <div class="relative pt-1v pb-2v">
+ <div class="bg-light absolute inset-0 mx-2v" style="z-index: -1">{{-- Inset light background --}}</div>
+
+ {{-- Just the URL: @image($hero_image, 'raw') --}}
+ @image($hero_image, 'full', ['class' => 'block mx-auto mb-1v'])
+
+ <div class="mx-auto mb-1v debug" style="max-width: 1040px">{!! nl2br($description) !!}</div>
+
+ @dump($gallery)
+
+ </div>
+
+ {{--
+ <footer>
+ {!! wp_link_pages(['echo' => 0, 'before' => '<nav class="page-nav"><p>' . __('Pages:', 'sage'), 'after' => '</p></nav>']) !!}
+ </footer>
+ --}}
+
+</article>
@section('content')
- <div class="container-content py-2v px-1v">
+ <div class="container-content py-2v">
@while(have_posts()) @php(the_post())
@includeFirst(['partials.content-single-' . get_post_type(), 'partials.content-single'])
--- /dev/null
+{{-- Re-use the Réalistions archive page for the filtered categories --}}
+@include('archive-realisation')
mix
.js('resources/assets/scripts/app.js', 'scripts')
.js('resources/assets/scripts/intro-carousel.js', 'scripts')
+ .js('resources/assets/scripts/masonry-columns.js', 'scripts')
.js('resources/assets/scripts/image-map.js', 'scripts')
.js('resources/assets/scripts/customizer.js', 'scripts')
.blocks('resources/assets/scripts/editor.js', 'scripts')
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
+columns.js@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/columns.js/-/columns.js-1.1.2.tgz#079c18d173fee71c47bc6904414f243a18f31c0c"
+ integrity sha512-ipdHiOn+ZcHH0WeRFywQIskoFzZd4CuUYRkcqX3Q3ry/nQpQeRToZ7bYyhAEXE+nU16umLvG7zjGjh/dQlfUKw==
+
commander@2.17.x:
version "2.17.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"