Skip to content

Commit

Permalink
Default responsive menu with caching (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
BobWez98 authored Nov 6, 2024
1 parent c0b9eaa commit cd8bb40
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 2 deletions.
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"require": {
"php": "^8.1|^8.2",
"rapidez/blade-directives": "^0.6",
"rapidez/core": "^2.13",
"justbetter/statamic-glide-directive": "^2.1",
"spatie/once": "*",
"statamic-rad-pack/runway": "^7.6",
Expand All @@ -54,7 +55,10 @@
"laravel": {
"providers": [
"Rapidez\\Statamic\\RapidezStatamicServiceProvider"
]
],
"aliases": {
"RapidezStatamic": "Rapidez\\Statamic\\Facades\\RapidezStatamic"
}
}
}
}
13 changes: 13 additions & 0 deletions config/rapidez/statamic.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@
],
],

'navigation' => [
// When a menu get's created, these collections will
// automaticly be added to the navigations allowed entries.
// This way, we don't need to update every navigation with
// settings and it will always be right.
'allowed_collections' => [
'pages',
'brand',
'category',
'product',
]
],

'sites' => [
'default' => [
'name' => env('APP_NAME', 'Statamic'),
Expand Down
65 changes: 65 additions & 0 deletions resources/views/components/nav-layer.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{{--
This is a mobile navigation slideover component for handling multi-level navigations. It makes use of recursion to handle multiple levels of navigation dynamically.
## Properties
- `id` Unique identifier for the current navigation layer.
- `children` Navigation children data
- `title` Title displayed in the header. Defaults to "Menu".
- `hasParent` Indicates if the current menu is a child of another menu. Defaults to false.
- `tag` Base HTML tag for the slideover component. Set this to 'form' for the main navigation or 'div' for nested menus.
- `parentUrl` URL used for navigating to the parent item when in a nested menu. Displayed with a "Go to" link.
## Slots
- `headerButton` This slot allows the customization of the button placed at the left in the header for navigation.
--}}
@props(['id', 'children', 'title' => __('Menu'), 'hasParent' => false, 'tag' => 'form', 'parentUrl' => ''])
@slots(['headerButton'])

<x-rapidez::slideover.mobile
:title="(string) $title"
:$id
:$hasParent
:$tag
>
<x-slot:headerButton>
<div class="absolute left-0 top-1/2 -translate-y-1/2 cursor-pointer text-white">
@include('rapidez-statamic::navigation.header-button')
</div>
</x-slot:headerButton>
<div class="bg-inactive-100 flex w-full flex-1 flex-col">
<ul class="mt-5 flex flex-col divide-y border-y bg-white">
@if ($hasParent && $parentUrl)
<li>
<a href="{{ $parentUrl }}" class="normal flex items-center justify-between p-5 font-semibold">
@lang('Go to :item', ['item' => strtolower($title)])
</a>
</li>
@endif
@foreach ($children ?: [] as $child)
<li class="relative">
@if ($child['title'] ?? '')
<a href="{{ $child['url'] }}" class="flex items-center justify-between p-5 font-semibold">
{{ $child['title'] }}
@if ($child['children'])
<x-heroicon-o-chevron-right class="size-4" />
@endif
</a>
@endif
@if ($child['children'])
@php($childId = uniqid(Str::snake("{$child['title']}" ?? '')))
<label class="absolute inset-0 cursor-pointer" for="{{ $childId }}"></label>
<x-rapidez-statamic::nav-layer
:id="$childId"
:children="$child['children'] ?? []"
:title="$child['title'] ?? ''"
:parent-url="$child['url']"
has-parent
tag="div"
></x-rapidez-statamic::nav-layer>
@endif
</li>
@endforeach
</ul>
{{ $slot }}
</div>
</x-rapidez::slideover.mobile>
49 changes: 49 additions & 0 deletions resources/views/components/nav.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@props(['nav' => 'nav:main', 'mobileNav' => 'nav:main'])
@php
// This combines multiple navigations into one for desktop and mobile seperately, only neccessary if you have multiple navigations
$navData = collect($nav)->flatMap(fn ($menu) => RapidezStatamic::nav($menu));
$mobileNavData = collect($mobileNav)->flatMap(fn ($menu) => RapidezStatamic::nav($menu));
@endphp
<nav class="lg:hidden">
<x-rapidez-statamic::nav-layer
id="navigation"
is-form
:children="$mobileNavData"
/>
</nav>

<nav class="relative border-y max-lg:hidden">
<ul class="text-neutral [&>:not(:hover)]:hover:text-inactive flex items-center justify-center gap-8 text-sm font-semibold">
@foreach ($navData as $item)
<li class="group">
<a class="relative flex py-5 transition" href="{{ $item['url'] }}">
{{ $item['title'] }}
@if ($item['children'])
<div class="bg-primary absolute inset-x-0 bottom-0 z-50 h-0.5 w-full origin-right translate-y-1/2 scale-x-0 transition group-hover:origin-left group-hover:scale-x-100"></div>
@endif
</a>
@if ($item['children'])
<div class="pointer-events-none absolute inset-x-0 top-full -translate-y-1 border-t bg-white opacity-0 transition group-hover:pointer-events-auto group-hover:translate-y-0 group-hover:opacity-100">
<div class="bg-neutral/50 pointer-events-none absolute inset-x-0 top-full h-screen"></div>
<div class="container relative flex overflow-hidden">
<ul class="w-full columns-3 flex-col gap-x-12 py-10 font-bold xl:columns-4">
@foreach ($item['children'] as $item)
<li class="flex break-inside-avoid flex-col gap-1 pb-5">
<a href="{{ $item['url'] }}">{{ $item['title'] }}</a>
<ul class="[&>:not(:hover)]:hover:text-inactive flex flex-col font-medium">
@foreach ($item['children'] as $item)
<li class="transition">
<a href="{{ $item['url'] }}">{{ $item['title'] }}</a>
</li>
@endforeach
</ul>
</li>
@endforeach
</ul>
</div>
</div>
@endif
</li>
@endforeach
</ul>
</nav>
1 change: 1 addition & 0 deletions resources/views/navigation/header-button.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{-- This is shown on the first slideover of the mobile menu, this is empty by default but can be overridden --}}
10 changes: 10 additions & 0 deletions src/Exceptions/NavException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Rapidez\Statamic\Exceptions;

use Exception;

class NavException extends Exception
{

}
17 changes: 17 additions & 0 deletions src/Facades/RapidezStatamic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Rapidez\Statamic\Facades;

use Illuminate\Support\Facades\Facade;
use Rapidez\Statamic\RapidezStatamic as RapidezStatamicFacade;

/**
* @see \Rapidez\Statamic\RapidezStatamic
*/
class RapidezStatamic extends Facade
{
protected static function getFacadeAccessor()
{
return RapidezStatamicFacade::class;
}
}
19 changes: 19 additions & 0 deletions src/Listeners/ClearNavTreeCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Rapidez\Statamic\Listeners;

use Illuminate\Support\Facades\Cache;
use Statamic\Events\NavTreeSaved;
use Statamic\Eloquent\Structures\NavTree;

class ClearNavTreeCache
{
public function handle(NavTreeSaved $event): void
{
/** @var NavTree $tree */
$tree = $event->tree;

Cache::forget('nav:' . $tree->handle() . '-' . config('rapidez.store'));
Cache::driver('array')->forget('global-link' . '-' . config('rapidez.store'));
}
}
19 changes: 19 additions & 0 deletions src/Listeners/SetCollectionsForNav.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Rapidez\Statamic\Listeners;

use Statamic\Events\NavCreated;
use Statamic\Structures\Nav;

class SetCollectionsForNav
{
public function handle(NavCreated $event): void
{
/** @var Nav $nav */
$nav = $event->nav;

$nav
->collections(config('rapidez.statamic.navigation.allowed_collections'))
->save();
}
}
87 changes: 87 additions & 0 deletions src/RapidezStatamic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace Rapidez\Statamic;

use Illuminate\Support\Facades\Cache;
use Statamic\Statamic;
use Statamic\Eloquent\Entries\Entry;
use Rapidez\Core\Facades\Rapidez;
use Rapidez\Statamic\Exceptions\NavException;
use Statamic\Structures\Page;

class RapidezStatamic
{
protected array $navCache = [];

public function nav(string $tag): array
{
throw_unless(
str($tag)->startsWith('nav:'),
NavException::class,
'You can only use a nav tag to get a navigation tree.'
);

return Cache::rememberForever($tag . '-' . config('rapidez.store'), fn() => $this->buildMenu($tag));
}

protected function buildMenu(string $key): array
{
$nav = Statamic::tag($key)->fetch();

$cacheKey = $key . '-' . config('rapidez.store');
$this->navCache[$key] = Cache::get($cacheKey, []);

return $this->buildTree($nav, $key);
}

protected function buildTree(array $items, string $nav): array
{
$tree = [];

foreach ($items as $item) {
if ($item['children']) {
$children = $this->buildTree($item['children'], $nav);
if ($children) {
$item['children'] = $children;
}
}

$item['url'] = $this->determineEntryUrl($item['entry_id']->augmentable(), $nav);

$tree[] = $item;
}

return $tree;
}

public function determineEntryUrl(Entry|Page $entry, string $nav = 'global-link'): string
{
$cacheKey = $nav . '-' . config('rapidez.store');

if ( ! isset($this->navCache[$nav][$entry->id()])) {
$linkedRunwayResourceKey = $entry
->data()
->keys()
->firstWhere(fn($field) => collect([
...config('rapidez.statamic.navigation.allowed_collections'),
'linked_',
])->firstWhere(fn($key) => str($field)->startsWith($key)));

if (!$linkedRunwayResourceKey || !$entry->{$linkedRunwayResourceKey} || $entry->slug()) {
return $entry->url() ?? '';
}

$suffix = match (true) {
str($linkedRunwayResourceKey)->contains('category') => Rapidez::config('catalog/seo/category_url_suffix', ''),
str($linkedRunwayResourceKey)->contains('product') => Rapidez::config('catalog/seo/product_url_suffix', ''),
default => '',
};

$this->navCache[$nav][$entry->id()] = '/' . $entry->{$linkedRunwayResourceKey}['url_path'] . $suffix;

Cache::forever($cacheKey, $this->navCache[$nav]);
}

return $this->navCache[$nav][$entry->id()];
}
}
10 changes: 9 additions & 1 deletion src/RapidezStatamicServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Rapidez\Statamic;

use Statamic\Statamic;
use Statamic\Sites\Sites;
use Statamic\Facades\Site;
use Statamic\Facades\Entry;
Expand All @@ -26,6 +25,10 @@
use Rapidez\Statamic\Http\Controllers\ImportsController;
use Rapidez\Statamic\Http\Controllers\StatamicRewriteController;
use Rapidez\Statamic\Http\ViewComposers\StatamicGlobalDataComposer;
use Rapidez\Statamic\Listeners\ClearNavTreeCache;
use Rapidez\Statamic\Listeners\SetCollectionsForNav;
use Statamic\Events\NavCreated;
use Statamic\Events\NavTreeSaved;
use TorMorten\Eventy\Facades\Eventy;

class RapidezStatamicServiceProvider extends ServiceProvider
Expand All @@ -35,6 +38,8 @@ public function register()
$this->app->extend(Sites::class, function () {
return new SitesLinkedToMagentoStores(config('statamic.sites'));
});

$this->app->singleton(RapidezStatamic::class);
}

public function boot()
Expand Down Expand Up @@ -97,6 +102,9 @@ public function bootListeners() : self
Cache::forget('statamic-globals-' . Site::selected()->handle());
});

Event::listen(NavCreated::class, SetCollectionsForNav::class);
Event::listen(NavTreeSaved::class, ClearNavTreeCache::class);

Eventy::addFilter('rapidez.statamic.category.entry.data', fn($category) => [
'title' => $category->name,
'slug' => trim($category->url_key),
Expand Down

0 comments on commit cd8bb40

Please sign in to comment.