Updated Search experience including adding fulltext mysql indicies.
Showing
15 changed files
with
291 additions
and
61 deletions
| ... | @@ -37,4 +37,9 @@ class Book extends Entity | ... | @@ -37,4 +37,9 @@ class Book extends Entity |
| 37 | return $pages->sortBy('priority'); | 37 | return $pages->sortBy('priority'); |
| 38 | } | 38 | } |
| 39 | 39 | ||
| 40 | + public function getExcerpt($length = 100) | ||
| 41 | + { | ||
| 42 | + return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description; | ||
| 43 | + } | ||
| 44 | + | ||
| 40 | } | 45 | } | ... | ... |
| ... | @@ -55,10 +55,30 @@ class Entity extends Model | ... | @@ -55,10 +55,30 @@ class Entity extends Model |
| 55 | return $this->getName() === strtolower($type); | 55 | return $this->getName() === strtolower($type); |
| 56 | } | 56 | } |
| 57 | 57 | ||
| 58 | + /** | ||
| 59 | + * Gets the class name. | ||
| 60 | + * @return string | ||
| 61 | + */ | ||
| 58 | public function getName() | 62 | public function getName() |
| 59 | { | 63 | { |
| 60 | $fullClassName = get_class($this); | 64 | $fullClassName = get_class($this); |
| 61 | return strtolower(array_slice(explode('\\', $fullClassName), -1, 1)[0]); | 65 | return strtolower(array_slice(explode('\\', $fullClassName), -1, 1)[0]); |
| 62 | } | 66 | } |
| 63 | 67 | ||
| 68 | + /** | ||
| 69 | + * Perform a full-text search on this entity. | ||
| 70 | + * @param string[] $fieldsToSearch | ||
| 71 | + * @param string[] $terms | ||
| 72 | + * @return mixed | ||
| 73 | + */ | ||
| 74 | + public static function fullTextSearch($fieldsToSearch, $terms) | ||
| 75 | + { | ||
| 76 | + $termString = ''; | ||
| 77 | + foreach($terms as $term) { | ||
| 78 | + $termString .= $term . '* '; | ||
| 79 | + } | ||
| 80 | + $fields = implode(',', $fieldsToSearch); | ||
| 81 | + return static::whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString])->get(); | ||
| 82 | + } | ||
| 83 | + | ||
| 64 | } | 84 | } | ... | ... |
| ... | @@ -143,20 +143,6 @@ class PageController extends Controller | ... | @@ -143,20 +143,6 @@ class PageController extends Controller |
| 143 | } | 143 | } |
| 144 | 144 | ||
| 145 | /** | 145 | /** |
| 146 | - * Search all available pages, Across all books. | ||
| 147 | - * @param Request $request | ||
| 148 | - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View | ||
| 149 | - */ | ||
| 150 | - public function searchAll(Request $request) | ||
| 151 | - { | ||
| 152 | - $searchTerm = $request->get('term'); | ||
| 153 | - if (empty($searchTerm)) return redirect()->back(); | ||
| 154 | - | ||
| 155 | - $pages = $this->pageRepo->getBySearch($searchTerm); | ||
| 156 | - return view('pages/search-results', ['pages' => $pages, 'searchTerm' => $searchTerm]); | ||
| 157 | - } | ||
| 158 | - | ||
| 159 | - /** | ||
| 160 | * Shows the view which allows pages to be re-ordered and sorted. | 146 | * Shows the view which allows pages to be re-ordered and sorted. |
| 161 | * @param $bookSlug | 147 | * @param $bookSlug |
| 162 | * @return \Illuminate\View\View | 148 | * @return \Illuminate\View\View | ... | ... |
app/Http/Controllers/SearchController.php
0 → 100644
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +namespace Oxbow\Http\Controllers; | ||
| 4 | + | ||
| 5 | +use Illuminate\Http\Request; | ||
| 6 | + | ||
| 7 | +use Oxbow\Http\Requests; | ||
| 8 | +use Oxbow\Http\Controllers\Controller; | ||
| 9 | +use Oxbow\Repos\BookRepo; | ||
| 10 | +use Oxbow\Repos\ChapterRepo; | ||
| 11 | +use Oxbow\Repos\PageRepo; | ||
| 12 | + | ||
| 13 | +class SearchController extends Controller | ||
| 14 | +{ | ||
| 15 | + protected $pageRepo; | ||
| 16 | + protected $bookRepo; | ||
| 17 | + protected $chapterRepo; | ||
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * SearchController constructor. | ||
| 21 | + * @param $pageRepo | ||
| 22 | + * @param $bookRepo | ||
| 23 | + * @param $chapterRepo | ||
| 24 | + */ | ||
| 25 | + public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo) | ||
| 26 | + { | ||
| 27 | + $this->pageRepo = $pageRepo; | ||
| 28 | + $this->bookRepo = $bookRepo; | ||
| 29 | + $this->chapterRepo = $chapterRepo; | ||
| 30 | + parent::__construct(); | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * Searches all entities. | ||
| 35 | + * @param Request $request | ||
| 36 | + * @return \Illuminate\View\View | ||
| 37 | + * @internal param string $searchTerm | ||
| 38 | + */ | ||
| 39 | + public function searchAll(Request $request) | ||
| 40 | + { | ||
| 41 | + if(!$request->has('term')) { | ||
| 42 | + return redirect()->back(); | ||
| 43 | + } | ||
| 44 | + $searchTerm = $request->get('term'); | ||
| 45 | + $pages = $this->pageRepo->getBySearch($searchTerm); | ||
| 46 | + $books = $this->bookRepo->getBySearch($searchTerm); | ||
| 47 | + $chapters = $this->chapterRepo->getBySearch($searchTerm); | ||
| 48 | + return view('search/all', ['pages' => $pages, 'books'=>$books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + | ||
| 52 | +} |
| ... | @@ -65,7 +65,7 @@ Route::group(['middleware' => 'auth'], function () { | ... | @@ -65,7 +65,7 @@ Route::group(['middleware' => 'auth'], function () { |
| 65 | Route::get('/link/{id}', 'PageController@redirectFromLink'); | 65 | Route::get('/link/{id}', 'PageController@redirectFromLink'); |
| 66 | 66 | ||
| 67 | // Search | 67 | // Search |
| 68 | - Route::get('/pages/search/all', 'PageController@searchAll'); | 68 | + Route::get('/search/all', 'SearchController@searchAll'); |
| 69 | 69 | ||
| 70 | // Other Pages | 70 | // Other Pages |
| 71 | Route::get('/', 'HomeController@index'); | 71 | Route::get('/', 'HomeController@index'); | ... | ... |
| ... | @@ -81,4 +81,17 @@ class BookRepo | ... | @@ -81,4 +81,17 @@ class BookRepo |
| 81 | return $slug; | 81 | return $slug; |
| 82 | } | 82 | } |
| 83 | 83 | ||
| 84 | + public function getBySearch($term) | ||
| 85 | + { | ||
| 86 | + $terms = explode(' ', preg_quote(trim($term))); | ||
| 87 | + $books = $this->book->fullTextSearch(['name', 'description'], $terms); | ||
| 88 | + $words = join('|', $terms); | ||
| 89 | + foreach ($books as $book) { | ||
| 90 | + //highlight | ||
| 91 | + $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $book->getExcerpt(100)); | ||
| 92 | + $book->searchSnippet = $result; | ||
| 93 | + } | ||
| 94 | + return $books; | ||
| 95 | + } | ||
| 96 | + | ||
| 84 | } | 97 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -67,4 +67,17 @@ class ChapterRepo | ... | @@ -67,4 +67,17 @@ class ChapterRepo |
| 67 | return $slug; | 67 | return $slug; |
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | + public function getBySearch($term) | ||
| 71 | + { | ||
| 72 | + $terms = explode(' ', preg_quote(trim($term))); | ||
| 73 | + $chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms); | ||
| 74 | + $words = join('|', $terms); | ||
| 75 | + foreach ($chapters as $chapter) { | ||
| 76 | + //highlight | ||
| 77 | + $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $chapter->getExcerpt(100)); | ||
| 78 | + $chapter->searchSnippet = $result; | ||
| 79 | + } | ||
| 80 | + return $chapters; | ||
| 81 | + } | ||
| 82 | + | ||
| 70 | } | 83 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -61,12 +61,35 @@ class PageRepo | ... | @@ -61,12 +61,35 @@ class PageRepo |
| 61 | 61 | ||
| 62 | public function getBySearch($term) | 62 | public function getBySearch($term) |
| 63 | { | 63 | { |
| 64 | - $terms = explode(' ', trim($term)); | 64 | + $terms = explode(' ', preg_quote(trim($term))); |
| 65 | - $query = $this->page; | 65 | + $pages = $this->page->fullTextSearch(['name', 'text'], $terms); |
| 66 | - foreach($terms as $term) { | 66 | + |
| 67 | - $query = $query->where('text', 'like', '%'.$term.'%'); | 67 | + // Add highlights to page text. |
| 68 | + $words = join('|', $terms); | ||
| 69 | + //lookahead/behind assertions ensures cut between words | ||
| 70 | + $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words | ||
| 71 | + | ||
| 72 | + foreach ($pages as $page) { | ||
| 73 | + preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER); | ||
| 74 | + //delimiter between occurrences | ||
| 75 | + $results = []; | ||
| 76 | + foreach ($matches as $line) { | ||
| 77 | + $results[] = htmlspecialchars($line[0], 0, 'UTF-8'); | ||
| 78 | + } | ||
| 79 | + $matchLimit = 6; | ||
| 80 | + if (count($results) > $matchLimit) { | ||
| 81 | + $results = array_slice($results, 0, $matchLimit); | ||
| 82 | + } | ||
| 83 | + $result = join('... ', $results); | ||
| 84 | + | ||
| 85 | + //highlight | ||
| 86 | + $result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result); | ||
| 87 | + if (strlen($result) < 5) { | ||
| 88 | + $result = $page->getExcerpt(80); | ||
| 89 | + } | ||
| 90 | + $page->searchSnippet = $result; | ||
| 68 | } | 91 | } |
| 69 | - return $query->get(); | 92 | + return $pages; |
| 70 | } | 93 | } |
| 71 | 94 | ||
| 72 | /** | 95 | /** |
| ... | @@ -95,7 +118,7 @@ class PageRepo | ... | @@ -95,7 +118,7 @@ class PageRepo |
| 95 | public function saveRevision(Page $page) | 118 | public function saveRevision(Page $page) |
| 96 | { | 119 | { |
| 97 | $lastRevision = $this->getLastRevision($page); | 120 | $lastRevision = $this->getLastRevision($page); |
| 98 | - if($lastRevision && ($lastRevision->html === $page->html && $lastRevision->name === $page->name)) { | 121 | + if ($lastRevision && ($lastRevision->html === $page->html && $lastRevision->name === $page->name)) { |
| 99 | return $page; | 122 | return $page; |
| 100 | } | 123 | } |
| 101 | $revision = $this->pageRevision->fill($page->toArray()); | 124 | $revision = $this->pageRevision->fill($page->toArray()); |
| ... | @@ -103,7 +126,7 @@ class PageRepo | ... | @@ -103,7 +126,7 @@ class PageRepo |
| 103 | $revision->created_by = Auth::user()->id; | 126 | $revision->created_by = Auth::user()->id; |
| 104 | $revision->save(); | 127 | $revision->save(); |
| 105 | // Clear old revisions | 128 | // Clear old revisions |
| 106 | - if($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { | 129 | + if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { |
| 107 | $this->pageRevision->where('page_id', '=', $page->id) | 130 | $this->pageRevision->where('page_id', '=', $page->id) |
| 108 | ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete(); | 131 | ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete(); |
| 109 | } | 132 | } |
| ... | @@ -141,7 +164,7 @@ class PageRepo | ... | @@ -141,7 +164,7 @@ class PageRepo |
| 141 | public function doesSlugExist($slug, $bookId, $currentId = false) | 164 | public function doesSlugExist($slug, $bookId, $currentId = false) |
| 142 | { | 165 | { |
| 143 | $query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId); | 166 | $query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId); |
| 144 | - if($currentId) { | 167 | + if ($currentId) { |
| 145 | $query = $query->where('id', '!=', $currentId); | 168 | $query = $query->where('id', '!=', $currentId); |
| 146 | } | 169 | } |
| 147 | return $query->count() > 0; | 170 | return $query->count() > 0; |
| ... | @@ -158,7 +181,7 @@ class PageRepo | ... | @@ -158,7 +181,7 @@ class PageRepo |
| 158 | public function findSuitableSlug($name, $bookId, $currentId = false) | 181 | public function findSuitableSlug($name, $bookId, $currentId = false) |
| 159 | { | 182 | { |
| 160 | $slug = Str::slug($name); | 183 | $slug = Str::slug($name); |
| 161 | - while($this->doesSlugExist($slug, $bookId, $currentId)) { | 184 | + while ($this->doesSlugExist($slug, $bookId, $currentId)) { |
| 162 | $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); | 185 | $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); |
| 163 | } | 186 | } |
| 164 | return $slug; | 187 | return $slug; | ... | ... |
| ... | @@ -71,7 +71,7 @@ class SettingService | ... | @@ -71,7 +71,7 @@ class SettingService |
| 71 | public function remove($key) | 71 | public function remove($key) |
| 72 | { | 72 | { |
| 73 | $setting = $this->getSettingObjectByKey($key); | 73 | $setting = $this->getSettingObjectByKey($key); |
| 74 | - if($setting) { | 74 | + if ($setting) { |
| 75 | $setting->delete(); | 75 | $setting->delete(); |
| 76 | } | 76 | } |
| 77 | return true; | 77 | return true; |
| ... | @@ -82,7 +82,8 @@ class SettingService | ... | @@ -82,7 +82,8 @@ class SettingService |
| 82 | * @param $key | 82 | * @param $key |
| 83 | * @return mixed | 83 | * @return mixed |
| 84 | */ | 84 | */ |
| 85 | - private function getSettingObjectByKey($key) { | 85 | + private function getSettingObjectByKey($key) |
| 86 | + { | ||
| 86 | return $this->setting->where('setting_key', '=', $key)->first(); | 87 | return $this->setting->where('setting_key', '=', $key)->first(); |
| 87 | } | 88 | } |
| 88 | 89 | ... | ... |
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +use Illuminate\Database\Schema\Blueprint; | ||
| 4 | +use Illuminate\Database\Migrations\Migration; | ||
| 5 | + | ||
| 6 | +class AddSearchIndexes extends Migration | ||
| 7 | +{ | ||
| 8 | + /** | ||
| 9 | + * Run the migrations. | ||
| 10 | + * | ||
| 11 | + * @return void | ||
| 12 | + */ | ||
| 13 | + public function up() | ||
| 14 | + { | ||
| 15 | + DB::statement('ALTER TABLE pages ADD FULLTEXT search(name, text)'); | ||
| 16 | + DB::statement('ALTER TABLE books ADD FULLTEXT search(name, description)'); | ||
| 17 | + DB::statement('ALTER TABLE chapters ADD FULLTEXT search(name, description)'); | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + /** | ||
| 21 | + * Reverse the migrations. | ||
| 22 | + * | ||
| 23 | + * @return void | ||
| 24 | + */ | ||
| 25 | + public function down() | ||
| 26 | + { | ||
| 27 | + Schema::table('pages', function(Blueprint $table) { | ||
| 28 | + $table->dropIndex('search'); | ||
| 29 | + }); | ||
| 30 | + Schema::table('books', function(Blueprint $table) { | ||
| 31 | + $table->dropIndex('search'); | ||
| 32 | + }); | ||
| 33 | + Schema::table('chapters', function(Blueprint $table) { | ||
| 34 | + $table->dropIndex('search'); | ||
| 35 | + }); | ||
| 36 | + } | ||
| 37 | +} |
| ... | @@ -211,6 +211,12 @@ p.secondary, p .secondary, span.secondary, .text-secondary { | ... | @@ -211,6 +211,12 @@ p.secondary, p .secondary, span.secondary, .text-secondary { |
| 211 | } | 211 | } |
| 212 | } | 212 | } |
| 213 | 213 | ||
| 214 | +span.highlight { | ||
| 215 | + //background-color: rgba($primary, 0.2); | ||
| 216 | + font-weight: bold; | ||
| 217 | + //padding: 2px 4px; | ||
| 218 | +} | ||
| 219 | + | ||
| 214 | /* | 220 | /* |
| 215 | * Lists | 221 | * Lists |
| 216 | */ | 222 | */ | ... | ... |
| ... | @@ -36,9 +36,6 @@ header { | ... | @@ -36,9 +36,6 @@ header { |
| 36 | padding-right: 0; | 36 | padding-right: 0; |
| 37 | } | 37 | } |
| 38 | } | 38 | } |
| 39 | - .search-box { | ||
| 40 | - padding-top: $-l *0.8; | ||
| 41 | - } | ||
| 42 | .avatar, .user-name { | 39 | .avatar, .user-name { |
| 43 | display: inline-block; | 40 | display: inline-block; |
| 44 | } | 41 | } |
| ... | @@ -59,6 +56,23 @@ header { | ... | @@ -59,6 +56,23 @@ header { |
| 59 | } | 56 | } |
| 60 | } | 57 | } |
| 61 | 58 | ||
| 59 | +form.search-box { | ||
| 60 | + padding-top: $-l *0.9; | ||
| 61 | + display: inline-block; | ||
| 62 | + input { | ||
| 63 | + background-color: transparent; | ||
| 64 | + border-radius: 0; | ||
| 65 | + border: none; | ||
| 66 | + border-bottom: 2px solid #EEE; | ||
| 67 | + color: #EEE; | ||
| 68 | + padding-left: $-l; | ||
| 69 | + outline: 0; | ||
| 70 | + } | ||
| 71 | + i { | ||
| 72 | + margin-right: -$-l; | ||
| 73 | + } | ||
| 74 | +} | ||
| 75 | + | ||
| 62 | #content { | 76 | #content { |
| 63 | display: block; | 77 | display: block; |
| 64 | position: relative; | 78 | position: relative; | ... | ... |
| ... | @@ -55,10 +55,15 @@ | ... | @@ -55,10 +55,15 @@ |
| 55 | <div class="col-md-3"> | 55 | <div class="col-md-3"> |
| 56 | <a href="/" class="logo">{{ Setting::get('app-name', 'BookStack') }}</a> | 56 | <a href="/" class="logo">{{ Setting::get('app-name', 'BookStack') }}</a> |
| 57 | </div> | 57 | </div> |
| 58 | - <div class="col-md-9"> | 58 | + <div class="col-md-3 text-right"> |
| 59 | + <form action="/search/all" method="GET" class="search-box"> | ||
| 60 | + <i class="zmdi zmdi-search"></i> | ||
| 61 | + <input type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}"> | ||
| 62 | + </form> | ||
| 63 | + </div> | ||
| 64 | + <div class="col-md-6"> | ||
| 59 | <div class="float right"> | 65 | <div class="float right"> |
| 60 | <div class="links text-center"> | 66 | <div class="links text-center"> |
| 61 | - <a href="/search"><i class="zmdi zmdi-search"></i></a> | ||
| 62 | <a href="/books"><i class="zmdi zmdi-book"></i>Books</a> | 67 | <a href="/books"><i class="zmdi zmdi-book"></i>Books</a> |
| 63 | @if($currentUser->can('settings-update')) | 68 | @if($currentUser->can('settings-update')) |
| 64 | <a href="/settings"><i class="zmdi zmdi-settings"></i>Settings</a> | 69 | <a href="/settings"><i class="zmdi zmdi-settings"></i>Settings</a> | ... | ... |
| 1 | -@extends('base') | ||
| 2 | - | ||
| 3 | -@section('content') | ||
| 4 | - | ||
| 5 | - | ||
| 6 | - <div class="row"> | ||
| 7 | - | ||
| 8 | - <div class="col-md-3 page-menu"> | ||
| 9 | - | ||
| 10 | - </div> | ||
| 11 | - | ||
| 12 | - <div class="col-md-9 page-content"> | ||
| 13 | - <h1>Search Results <span class="subheader">For '{{$searchTerm}}'</span></h1> | ||
| 14 | - <div class="page-list"> | ||
| 15 | - @if(count($pages) > 0) | ||
| 16 | - @foreach($pages as $page) | ||
| 17 | - <a href="{{$page->getUrl() . '#' . $searchTerm}}">{{$page->name}}</a> | ||
| 18 | - @endforeach | ||
| 19 | - @else | ||
| 20 | - <p class="text-muted">No pages matched this search</p> | ||
| 21 | - @endif | ||
| 22 | - </div> | ||
| 23 | - </div> | ||
| 24 | - | ||
| 25 | - </div> | ||
| 26 | - | ||
| 27 | - | ||
| 28 | - | ||
| 29 | - | ||
| 30 | -@stop | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
resources/views/search/all.blade.php
0 → 100644
| 1 | +@extends('base') | ||
| 2 | + | ||
| 3 | +@section('content') | ||
| 4 | + | ||
| 5 | + <div class="container"> | ||
| 6 | + | ||
| 7 | + <h1>Search Results <span class="text-muted">{{$searchTerm}}</span></h1> | ||
| 8 | + | ||
| 9 | + <div class="row"> | ||
| 10 | + | ||
| 11 | + <div class="col-md-6"> | ||
| 12 | + <h3>Matching Pages</h3> | ||
| 13 | + <div class="page-list"> | ||
| 14 | + @if(count($pages) > 0) | ||
| 15 | + @foreach($pages as $page) | ||
| 16 | + <div class="book-child"> | ||
| 17 | + <h3> | ||
| 18 | + <a href="{{$page->getUrl() . '#' . $searchTerm}}" class="page"> | ||
| 19 | + <i class="zmdi zmdi-file-text"></i>{{$page->name}} | ||
| 20 | + </a> | ||
| 21 | + </h3> | ||
| 22 | + <p class="text-muted"> | ||
| 23 | + {!! $page->searchSnippet !!} | ||
| 24 | + </p> | ||
| 25 | + <hr> | ||
| 26 | + </div> | ||
| 27 | + @endforeach | ||
| 28 | + @else | ||
| 29 | + <p class="text-muted">No pages matched this search</p> | ||
| 30 | + @endif | ||
| 31 | + </div> | ||
| 32 | + </div> | ||
| 33 | + | ||
| 34 | + <div class="col-md-5 col-md-offset-1"> | ||
| 35 | + | ||
| 36 | + @if(count($books) > 0) | ||
| 37 | + <h3>Matching Books</h3> | ||
| 38 | + <div class="page-list"> | ||
| 39 | + @foreach($books as $book) | ||
| 40 | + <div class="book-child"> | ||
| 41 | + <h3> | ||
| 42 | + <a href="{{$book->getUrl()}}" class="text-book"> | ||
| 43 | + <i class="zmdi zmdi-book"></i>{{$book->name}} | ||
| 44 | + </a> | ||
| 45 | + </h3> | ||
| 46 | + <p class="text-muted"> | ||
| 47 | + {!! $book->searchSnippet !!} | ||
| 48 | + </p> | ||
| 49 | + <hr> | ||
| 50 | + </div> | ||
| 51 | + @endforeach | ||
| 52 | + </div> | ||
| 53 | + @endif | ||
| 54 | + | ||
| 55 | + @if(count($chapters) > 0) | ||
| 56 | + <h3>Matching Chapters</h3> | ||
| 57 | + <div class="page-list"> | ||
| 58 | + @foreach($chapters as $chapter) | ||
| 59 | + <div class="book-child"> | ||
| 60 | + <h3> | ||
| 61 | + <a href="{{$chapter->getUrl()}}" class="text-chapter"> | ||
| 62 | + <i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->name}} | ||
| 63 | + </a> | ||
| 64 | + </h3> | ||
| 65 | + <p class="text-muted"> | ||
| 66 | + {!! $chapter->searchSnippet !!} | ||
| 67 | + </p> | ||
| 68 | + <hr> | ||
| 69 | + </div> | ||
| 70 | + @endforeach | ||
| 71 | + </div> | ||
| 72 | + @endif | ||
| 73 | + | ||
| 74 | + </div> | ||
| 75 | + | ||
| 76 | + | ||
| 77 | + </div> | ||
| 78 | + | ||
| 79 | + | ||
| 80 | + </div> | ||
| 81 | + | ||
| 82 | + | ||
| 83 | + | ||
| 84 | + | ||
| 85 | +@stop | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or sign in to post a comment