Skip to content

Commit f1db87a

Browse files
committed
Implement full site search page
1 parent 91f3dae commit f1db87a

File tree

11 files changed

+286
-2
lines changed

11 files changed

+286
-2
lines changed

src/Controllers/Search/Search.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace BNETDocs\Controllers\Search;
4+
5+
use \BNETDocs\Libraries\Core\Router;
6+
use \BNETDocs\Libraries\Search\Search as SearchLib;
7+
8+
class Search extends \BNETDocs\Controllers\Base
9+
{
10+
/**
11+
* Constructs a Controller, typically to initialize properties.
12+
*/
13+
public function __construct()
14+
{
15+
$this->model = new \BNETDocs\Models\Search\Search();
16+
}
17+
18+
/**
19+
* Invoked by the Router class to handle the request.
20+
*
21+
* @param array|null $args The optional route arguments and any captured URI arguments.
22+
* @return boolean Whether the Router should invoke the configured View.
23+
*/
24+
public function invoke(?array $args): bool
25+
{
26+
$request_args = Router::query();
27+
$this->model->user_input = $request_args['q'] ?? null;
28+
29+
if (!empty($this->model->user_input))
30+
{
31+
$this->model->results = SearchLib::query($this->model->user_input);
32+
}
33+
34+
$this->model->_responseCode = \BNETDocs\Libraries\Core\HttpCode::HTTP_OK;
35+
return true;
36+
}
37+
}

src/Libraries/Search/Results.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,18 @@ public function getUsers(): array
157157
return $this->users;
158158
}
159159

160+
public function isEmpty(): bool
161+
{
162+
if (!empty($this->comments)) return false;
163+
if (!empty($this->documents)) return false;
164+
if (!empty($this->news_posts)) return false;
165+
if (!empty($this->packets)) return false;
166+
if (!empty($this->servers)) return false;
167+
if (!empty($this->users)) return false;
168+
169+
return true;
170+
}
171+
160172
public function jsonSerialize(): mixed
161173
{
162174
return [

src/Libraries/Search/Search.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
namespace BNETDocs\Libraries\Search;
44

55
use \BNETDocs\Libraries\Comment;
6-
use \BNETDocs\Libraries\Db\MariaDb;
76
use \BNETDocs\Libraries\Document;
87
use \BNETDocs\Libraries\News\Post as NewsPost;
98
use \BNETDocs\Libraries\Packet\Packet;
9+
use \BNETDocs\Libraries\Search\Results;
1010
use \BNETDocs\Libraries\Server\Server;
1111
use \BNETDocs\Libraries\User\User;
1212

@@ -17,7 +17,7 @@ private function __construct() {}
1717
public static function query(string $user_input): Results|null
1818
{
1919
$results = new Results();
20-
$pdo = MariaDb::instance();
20+
$pdo = \BNETDocs\Libraries\Db\MariaDb::instance();
2121

2222
$term = trim($user_input);
2323
if ($term === '') return null;

src/Libraries/Tag/Tag.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public function __construct(?object $value)
2323
}
2424
}
2525

26+
public function __toString(): string
27+
{
28+
return $this->getTagString() ?? '';
29+
}
30+
2631
public function allocate(): bool
2732
{
2833
$p = [

src/Models/Search/Search.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace BNETDocs\Models\Search;
4+
5+
class Search extends \BNETDocs\Models\ActiveUser implements \JsonSerializable
6+
{
7+
public ?string $user_input = null;
8+
public ?\BNETDocs\Libraries\Search\Results $results = null;
9+
10+
public function jsonSerialize(): mixed
11+
{
12+
return \array_merge(parent::jsonSerialize(), [
13+
'results' => $this->results,
14+
'user_input' => $this->user_input,
15+
]);
16+
}
17+
}

src/Templates/Includes/header.inc.phtml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ function _header_active(string $url, bool $sr)
3535
if (!$match) return '';
3636
if ($sr) return ' <span class="sr-only">(current)</span>'; else return ' active';
3737
}
38+
function _header_search_query(): string
39+
{
40+
$current_url = parse_url(getenv('REQUEST_URI'), PHP_URL_PATH);
41+
$match = (substr($current_url, 0, strlen('/search')) == '/search');
42+
if (!$match) return '';
43+
return \BNETDocs\Libraries\Core\Router::query()['q'] ?? '';
44+
}
3845
$_header_nav = [
3946
['label' => 'Welcome', 'url' => '/welcome'],
4047
['label' => 'Community', 'dropdown' => [
@@ -178,6 +185,9 @@ function _header_nav_html($nav)
178185
// Navbar (End)
179186
_line('</ul>');
180187

188+
// Search
189+
_line('<form action="%s" method="GET" class="mb-3 mb-lg-0 me-lg-3" role="search"><input name="q" type="search" class="form-control bg-dark border-dark text-light" placeholder="Search…" aria-label="Search" value="%s"></form>', UrlFormatter::format('/search'), _header_search_query());
190+
181191
// Account
182192
if (!Authentication::$user)
183193
{

src/Templates/Search/Search.phtml

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
namespace BNETDocs\Templates\Search;
3+
$title = 'Search';
4+
$results = $this->getContext()->results;
5+
$user_input = $this->getContext()->user_input ?? '';
6+
if ($results)
7+
{
8+
$sections = [
9+
'Comments' => $results->getComments(),
10+
'Documents' => $results->getDocuments(),
11+
'News Posts' => $results->getNewsPosts(),
12+
'Packets' => $results->getPackets(),
13+
'Servers' => $results->getServers(),
14+
'Users' => $results->getUsers(),
15+
];
16+
}
17+
require('./Includes/header.inc.phtml'); ?>
18+
<div class="container">
19+
<?php if (!$results instanceof \BNETDocs\Libraries\Search\Results || $results->isEmpty()): ?>
20+
<h1 class="mt-4 mb-3">No Results</h1>
21+
<p class="text-muted">Your search for <code><?= filter_var($user_input, FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?></code> returned no results.</p>
22+
<?php else:
23+
$nonEmptyCount = 0;
24+
foreach ($sections as $items) {
25+
if (!empty($items)) $nonEmptyCount++;
26+
}
27+
$collapse_all_by_default = ($nonEmptyCount >= 2);
28+
?>
29+
<h1 class="mt-4 mb-3">Search Results</h1>
30+
<p class="text-muted">Query: <code><?= filter_var($user_input, FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?></code></p>
31+
32+
<div class="mb-3">
33+
<button class="btn btn-sm btn-outline-secondary mr-2" id="expand-all">Expand All</button>
34+
<button class="btn btn-sm btn-outline-secondary" id="collapse-all">Collapse All</button>
35+
</div>
36+
37+
<div id="searchAccordion">
38+
<?php foreach ($sections as $label => $items): ?>
39+
<?php if (!empty($items)): ?>
40+
<?php $collapseId = strtolower(str_replace(' ', '-', $label)); ?>
41+
<div class="card mb-3">
42+
<div class="card-header" id="heading-<?= $collapseId ?>">
43+
<h5 class="mb-0">
44+
<button class="btn btn-link text-white" data-toggle="collapse" data-target="#collapse-<?= $collapseId ?>" aria-expanded="<?= $collapse_all_by_default ? 'false' : 'true' ?>" aria-controls="collapse-<?= $collapseId ?>">
45+
<?= $label ?> (<?= count($items) ?>)
46+
</button>
47+
</h5>
48+
</div>
49+
<div id="collapse-<?= $collapseId ?>" class="collapse<?= $collapse_all_by_default ? '' : ' show' ?>" aria-labelledby="heading-<?= $collapseId ?>" data-parent="#searchAccordion">
50+
<div class="card-body p-0">
51+
<?php if ($label === 'Comments'): ?>
52+
<?php
53+
$comments = $items;
54+
$comment_show_parent = true;
55+
require('./Comment/Section.inc.phtml');
56+
?>
57+
<?php else: ?>
58+
<ul class="list-group list-group-flush">
59+
<?php foreach ($items as $item): ?>
60+
<li class="list-group-item">
61+
<?php
62+
$url = '#';
63+
$title = 'Untitled';
64+
$preview = '';
65+
$tags = [];
66+
67+
switch (true) {
68+
case $item instanceof \BNETDocs\Libraries\Document:
69+
$url = $item->getURI();
70+
$title = $item->getTitle();
71+
$preview = $item->getBrief(true) ?: $item->getContent(true);
72+
$tags = $item->getTags();
73+
break;
74+
75+
case $item instanceof \BNETDocs\Libraries\News\Post:
76+
$url = $item->getURI();
77+
$title = $item->getTitle();
78+
$preview = $item->getContent(true);
79+
$tags = $item->getTags();
80+
break;
81+
82+
case $item instanceof \BNETDocs\Libraries\Packet\Packet:
83+
$url = $item->getURI();
84+
$title = $item->getLabel();
85+
$preview = $item->getFormat() ?: $item->getRemarks(true);
86+
$tags = $item->getTags();
87+
break;
88+
89+
case $item instanceof \BNETDocs\Libraries\Server\Server:
90+
$url = $item->getURI();
91+
$title = $item->getLabel() . ' (' . $item->getAddress() . ':' . $item->getPort() . ')';
92+
$tags = $item->getTags();
93+
break;
94+
95+
case $item instanceof \BNETDocs\Libraries\User\User:
96+
$url = $item->getURI();
97+
$title = $item->getName();
98+
$tags = $item->getTags();
99+
break;
100+
101+
default:
102+
$title = 'ID ' . $item->getId();
103+
break;
104+
}
105+
106+
$titleSafe = filter_var($title, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
107+
$previewSafe = filter_var(mb_strimwidth(strip_tags($preview), 0, 200, ''), FILTER_SANITIZE_FULL_SPECIAL_CHARS);
108+
?>
109+
<a href="<?= $url ?>"><strong><?= $titleSafe ?></strong></a>
110+
<?php if (!empty($previewSafe)): ?>
111+
<div class="text-muted small mt-1"><?= $previewSafe ?></div>
112+
<?php endif; ?>
113+
<?php if (!empty($tags)): ?>
114+
<div class="mt-2">
115+
<?php foreach ($tags as $tag): ?>
116+
<span class="badge badge-secondary"><?= filter_var($tag, FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?></span>
117+
<?php endforeach; ?>
118+
</div>
119+
<?php endif; ?>
120+
</li>
121+
<?php endforeach; ?>
122+
</ul>
123+
<?php endif; ?>
124+
</div>
125+
</div>
126+
</div>
127+
<?php endif; ?>
128+
<?php endforeach; ?>
129+
</div>
130+
<?php endif; ?>
131+
</div>
132+
133+
<script>
134+
document.addEventListener('DOMContentLoaded', function () {
135+
document.getElementById('expand-all').addEventListener('click', function () {
136+
document.querySelectorAll('#searchAccordion .collapse').forEach(el => {
137+
if (!el.classList.contains('show')) $(el).collapse('show');
138+
});
139+
});
140+
141+
document.getElementById('collapse-all').addEventListener('click', function () {
142+
document.querySelectorAll('#searchAccordion .collapse').forEach(el => {
143+
if (el.classList.contains('show')) $(el).collapse('hide');
144+
});
145+
});
146+
});
147+
</script>
148+
<?php require('./Includes/footer.inc.phtml'); ?>

src/Views/Search/SearchHtml.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace BNETDocs\Views\Search;
4+
5+
class SearchHtml extends \BNETDocs\Views\Base\Html
6+
{
7+
public static function invoke(\BNETDocs\Interfaces\Model $model): void
8+
{
9+
if (!$model instanceof \BNETDocs\Models\Search\Search)
10+
{
11+
throw new \BNETDocs\Exceptions\InvalidModelException($model);
12+
}
13+
14+
(new \BNETDocs\Libraries\Core\Template($model, 'Search/Search'))->invoke();
15+
$model->_responseHeaders['Content-Type'] = self::mimeType();
16+
}
17+
}

src/Views/Search/SearchJson.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace BNETDocs\Views\Search;
4+
5+
class SearchJson extends \BNETDocs\Views\Base\Json
6+
{
7+
public static function invoke(\BNETDocs\Interfaces\Model $model): void
8+
{
9+
if (!$model instanceof \BNETDocs\Models\Search\Search)
10+
{
11+
throw new \BNETDocs\Exceptions\InvalidModelException($model);
12+
}
13+
14+
echo json_encode($model, self::jsonFlags());
15+
$model->_responseHeaders['Content-Type'] = self::mimeType();
16+
}
17+
}

src/Views/Search/SearchPlain.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace BNETDocs\Views\Search;
4+
5+
class SearchPlain extends \BNETDocs\Views\Base\Plain
6+
{
7+
public static function invoke(\BNETDocs\Interfaces\Model $model): void
8+
{
9+
if (!$model instanceof \BNETDocs\Models\Search\Search)
10+
{
11+
throw new \BNETDocs\Exceptions\InvalidModelException($model);
12+
}
13+
14+
echo 'TODO';
15+
$model->_responseHeaders['Content-Type'] = self::mimeType();
16+
}
17+
}

0 commit comments

Comments
 (0)