mirror of
https://codeberg.org/vlw/vlw.se.git
synced 2025-09-14 05:13:46 +02:00
wip: 2025-02-01T13:48:23+0100 (1738414103)
This commit is contained in:
parent
6a92a0bbeb
commit
0dd247408e
7 changed files with 177 additions and 179 deletions
|
@ -1,31 +1,26 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Reflect\Call;
|
use vlw\MySQL\Operators;
|
||||||
use Reflect\{Response, Path};
|
use Reflect\{Response, Path};
|
||||||
use ReflectRules\{Ruleset, Rules, Type};
|
use ReflectRules\{Ruleset, Rules, Type};
|
||||||
|
|
||||||
use VLW\API\Endpoints;
|
|
||||||
|
|
||||||
use VLW\Database\Database;
|
use VLW\Database\Database;
|
||||||
use VLW\Database\Tables\Work\WorkTable;
|
use VLW\Database\Tables\Search\SearchTable;
|
||||||
|
|
||||||
require_once Path::root("src/Endpoints.php");
|
|
||||||
require_once Path::root("src/Database/Database.php");
|
require_once Path::root("src/Database/Database.php");
|
||||||
require_once Path::root("src/Database/Tables/Work/Work.php");
|
require_once Path::root("src/Database/Tables/Search/Search.php");
|
||||||
|
|
||||||
class GET_Search extends Database {
|
class GET_Search extends Database {
|
||||||
const GET_QUERY = "q";
|
|
||||||
|
|
||||||
protected Ruleset $ruleset;
|
protected Ruleset $ruleset;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->ruleset = new Ruleset(strict: true);
|
$this->ruleset = new Ruleset(strict: true);
|
||||||
|
|
||||||
$this->ruleset->GET([
|
$this->ruleset->GET([
|
||||||
(new Rules(self::GET_QUERY))
|
(new Rules(SearchTable::QUERY->value))
|
||||||
->required()
|
->required()
|
||||||
->type(Type::STRING)
|
->type(Type::STRING)
|
||||||
->min(1)
|
->min(2)
|
||||||
->max(parent::SIZE_VARCHAR)
|
->max(parent::SIZE_VARCHAR)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -34,22 +29,23 @@
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function search_work(): Response {
|
|
||||||
return (new Call(Endpoints::WORK->value))->params([
|
|
||||||
WorkTable::TITLE->value => $_GET[self::GET_QUERY],
|
|
||||||
WorkTable::SUMMARY->value => $_GET[self::GET_QUERY]
|
|
||||||
])->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function main(): Response {
|
public function main(): Response {
|
||||||
$results = [
|
$result = $this->db
|
||||||
Endpoints::WORK->value => $this->search_work()->output()
|
->for(SearchTable::NAME)
|
||||||
];
|
->where([
|
||||||
|
SearchTable::QUERY->value => [
|
||||||
|
Operators::LIKE->value => "%{$_GET[SearchTable::QUERY->value]}%"
|
||||||
|
]
|
||||||
|
])
|
||||||
|
->select([
|
||||||
|
SearchTable::TITLE->value,
|
||||||
|
SearchTable::SUMMARY->value,
|
||||||
|
SearchTable::CATEGORY->value,
|
||||||
|
SearchTable::HREF->value
|
||||||
|
]);
|
||||||
|
|
||||||
// Calculate the total number of results from all searched endpoints
|
return $result->num_rows > 0
|
||||||
$num_results = array_sum(array_map(fn(array $result): int => count($result), array_values($results)));
|
? new Response($result->fetch_all(MYSQLI_ASSOC))
|
||||||
|
: new Response([], 404);
|
||||||
// Return 404 if no search results
|
|
||||||
return new Response($results, $num_results > 0 ? 200 : 404);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,85 +1,57 @@
|
||||||
/* # Overrides */
|
vv-shell[vv-page="/search"] {
|
||||||
|
|
||||||
[vv-page="/search"]:not(body) {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--padding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* # Sections */
|
/* # Search */
|
||||||
|
|
||||||
/* ## Search */
|
|
||||||
|
|
||||||
section.search {
|
section.search {
|
||||||
width: 100%;
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--padding);
|
|
||||||
background-color: rgba(255, 255, 255, .05);
|
|
||||||
padding: calc(var(--padding) * 1.5);
|
|
||||||
margin-bottom: calc(var(--padding) * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
vv-shell[vv-page="/search"] > section.search {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: var(--padding);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: var(--padding);
|
||||||
|
background-color: rgba(255, 255, 255, .1);
|
||||||
}
|
}
|
||||||
|
|
||||||
section.search form {
|
section.search form {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
section.search search {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.search input {
|
section.search input {
|
||||||
width: 100%;
|
flex: 1 1 auto;
|
||||||
border: none;
|
|
||||||
color: black;
|
|
||||||
outline: none;
|
|
||||||
padding: var(--padding);
|
|
||||||
background-color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
section.search button[type="submit"] {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.search > svg {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* # Search results */
|
|
||||||
|
|
||||||
section.results .result {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: calc(var(--padding) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ## Titles */
|
|
||||||
|
|
||||||
section.title a h2 {
|
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
section.title a h2::before {
|
|
||||||
content: "// ";
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ## Work */
|
|
||||||
|
|
||||||
section.results.work {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: calc(var(--padding) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
section.results.work .result {
|
|
||||||
padding: var(--padding);
|
|
||||||
background-color: rgba(255, 255, 255, .03);
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
padding: 0 var(--padding);
|
||||||
|
border: solid 2px rgba(255, 255, 255, .1);
|
||||||
|
background-color: rgba(255, 255, 255, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.search input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.search select {
|
||||||
|
padding: 5px;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.search select :is(option, optgroup) {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* # Start search */
|
||||||
|
|
||||||
|
section.start-search {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
fill: var(--color-accent);
|
||||||
|
gap: calc(var(--padding) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.start-search svg {
|
||||||
|
width: 60px;
|
||||||
}
|
}
|
|
@ -1,95 +1,59 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Vegvisir\Path;
|
use const VLW\ICONS_DIR;
|
||||||
|
use VLW\Database\Models\Search\Search;
|
||||||
|
use VLW\Database\Tables\Search\{SearchTable, SearchCategoryEnum};
|
||||||
|
|
||||||
use VLW\Client\API;
|
require_once VV::root("src/Consts.php");
|
||||||
use VLW\API\Endpoints;
|
require_once VV::root("src/Database/Tables/Search/Search.php");
|
||||||
|
require_once VV::root("src/Database/Models/Search/Search.php");
|
||||||
|
|
||||||
use VLW\Database\Tables\Work\{
|
$search = new class extends Search {
|
||||||
WorkTable,
|
public function __construct() {}
|
||||||
ActionsTable
|
|
||||||
};
|
|
||||||
|
|
||||||
require_once VV::root("src/client/API.php");
|
public static function get_query(): ?string {
|
||||||
require_once VV::root("api/src/Endpoints.php");
|
return $_GET[SearchTable::QUERY->value] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
require_once VV::root("api/src/Database/Tables/Work/Work.php");
|
public static function get_category(): ?SearchCategoryEnum {
|
||||||
require_once VV::root("api/src/Database/Tables/Work/WorkActions.php");
|
return SearchCategoryEnum::tryFromName($_GET[SearchTable::CATEGORY->value] ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
// Search endpoint query paramter
|
public function search(): array {
|
||||||
const SEARCH_PARAM = "q";
|
$results = parent::all([SearchTable::QUERY->value => self::get_query()]);
|
||||||
|
}
|
||||||
// Connect to VLW API
|
}
|
||||||
$api = new API();
|
|
||||||
|
|
||||||
// Get search results from endpoint
|
|
||||||
$response = $api->call(Endpoints::SEARCH->value)->params([SEARCH_PARAM => $_GET[SEARCH_PARAM]])->get();
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<style><?= VV::css("public/assets/css/pages/search") ?></style>
|
<style><?= VV::css("public/assets/css/pages/search") ?></style>
|
||||||
<section class="search">
|
<section class="search">
|
||||||
<form method="GET">
|
<form>
|
||||||
<search>
|
<input name="<?= SearchTable::QUERY->value ?>" type="search" placeholder="search vlw.se..." value="<?= $search::get_query() ?>">
|
||||||
<input name="<? SEARCH_PARAM ?>" type="text" placeholder="search vlw.se..." value="<?= $_GET[SEARCH_PARAM] ?>"></input>
|
<select name="<?= SearchTable::CATEGORY->value ?>">
|
||||||
</search>
|
|
||||||
<button type="submit" class="inline solid">Search</button>
|
<option value="null">All</option>
|
||||||
|
<optgroup label="Categories">
|
||||||
|
|
||||||
|
<?php foreach (SearchCategoryEnum::names() as $category): ?>
|
||||||
|
<?php $category = SearchCategoryEnum::fromName($category); ?>
|
||||||
|
<option value="<?= $category->name ?>" <?= $search::get_category() === $category ? "selected" : "" ?>><?= ucfirst(strtolower($category->name)) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
</optgroup>
|
||||||
|
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="inline solid"><?= VV::embed(ICONS_DIR . "search.svg") ?></button>
|
||||||
</form>
|
</form>
|
||||||
<?= VV::embed("public/assets/media/line.svg") ?>
|
|
||||||
<button class="inline">advanced search options</button>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<?php if ($response->ok): ?>
|
<?php if (array_key_exists(SearchTable::QUERY->value, $_GET)): ?>
|
||||||
<?php // Get response body ?>
|
<section class="results">
|
||||||
<?php $results = $response->json(); ?>
|
|
||||||
|
|
||||||
<?php // Search contains results from the work endpoint ?>
|
|
||||||
<?php if ($results[Endpoints::WORK->value]): ?>
|
|
||||||
<section class="title work">
|
|
||||||
<a href="<? Endpoints::WORK->value ?>"><h2>Work</h2></a>
|
|
||||||
<p><?= count($results[Endpoints::WORK->value]) ?> search result(s) from my public work</p>
|
|
||||||
</section>
|
</section>
|
||||||
<section class="results work">
|
|
||||||
|
|
||||||
<?php // List all work category search results ?>
|
|
||||||
<?php foreach ($results[Endpoints::WORK->value] as $result): ?>
|
|
||||||
<div class="result">
|
|
||||||
<h3><?= $result[WorkTable::TITLE->value] ?></h3>
|
|
||||||
<p><?= $result[WorkTable::SUMMARY->value] ?></p>
|
|
||||||
<p><?= date(API::DATE_FORMAT, $result[WorkTable::DATE_CREATED->value]) ?></p>
|
|
||||||
|
|
||||||
<?php // Get action buttons for work entity by id ?>
|
|
||||||
<?php $actions = $api->call(Endpoints::WORK_ACTIONS->value)->params([ActionsTable::REF_WORK_ID->value => $result[WorkTable::ID->value]])->get(); ?>
|
|
||||||
|
|
||||||
<?php // List each action button for work entity if exists ?>
|
|
||||||
<?php if ($actions->ok): ?>
|
|
||||||
<div class="actions">
|
|
||||||
<?php foreach ($actions->json() as $action): ?>
|
|
||||||
|
|
||||||
<?php // Bind VV Interactions if link is same origin, else open in new tab ?>
|
|
||||||
<a href="<?= $action[ActionsTable::HREF->value] ?>" target="_blank"><button class="inline <?= $action[ActionsTable::CLASS_LIST->value] ?>"><?= $action[ActionsTable::DISPLAY_TEXT->value] ?></button></a>
|
|
||||||
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
<section class="start-search">
|
||||||
<?php if (!empty($_GET[SEARCH_PARAM])): ?>
|
<?= VV::embed(ICONS_DIR . "search.svg") ?>
|
||||||
<section class="info noresults">
|
|
||||||
<img src="/assets/media/travolta.gif" alt="">
|
|
||||||
<p>No results for search term "<?= $_GET[SEARCH_PARAM] ?>"</p>
|
|
||||||
</section>
|
|
||||||
<?php else: ?>
|
|
||||||
<section class="info empty">
|
|
||||||
<?= VV::embed("public/assets/media/icons/search.svg") ?>
|
|
||||||
<p>Start typing to search</p>
|
<p>Start typing to search</p>
|
||||||
</section>
|
</section>
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<script><?= VV::js("public/assets/js/pages/search") ?></script>
|
<script><?= VV::js("public/assets/js/pages/search") ?></script>
|
42
src/Database/Models/Search/Search.php
Normal file
42
src/Database/Models/Search/Search.php
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VLW\Database\Models\Search;
|
||||||
|
|
||||||
|
use \VV;
|
||||||
|
|
||||||
|
use VLW\API\Endpoints;
|
||||||
|
use VLW\Database\Models\Model;
|
||||||
|
use VLW\Database\Tables\Search\{SearchTable, SearchCategoryEnum};
|
||||||
|
|
||||||
|
require_once VV::root("src/Consts.php");
|
||||||
|
require_once VV::root("src/API/Endpoints.php");
|
||||||
|
require_once VV::root("src/Database/Models/Model.php");
|
||||||
|
require_once VV::root("src/Database/Tables/Search/Search.php");
|
||||||
|
|
||||||
|
class Search extends Model {
|
||||||
|
public function __construct(public readonly string $id) {
|
||||||
|
parent::__construct(Endpoints::SEARCH, [
|
||||||
|
SearchTable::ID->value => $this->id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function all(array $params = []): array {
|
||||||
|
return array_map(fn(array $item): Search => new Search($item[SearchTable::ID->value]), parent::list(Endpoints::SEARCH, $params));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): ?string {
|
||||||
|
return $this->get(Search::TITLE->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function summary(): string {
|
||||||
|
return $this->get(Search::SUMMARY->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function category(): ?SearchCategoryEnum {
|
||||||
|
return SearchCategoryEnum::tryFromName($this->get(Search::CATEGORY->value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function href(): ?string {
|
||||||
|
return $this->get(Search::HREF->value);
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,6 +30,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
public function label(): TagsLabelEnum {
|
public function label(): TagsLabelEnum {
|
||||||
return TagsLabelEnum::from($this->get(TagsTable::LABEL->value));
|
return TagsLabelEnum::fromName($this->get(TagsTable::LABEL->value));
|
||||||
}
|
}
|
||||||
}
|
}
|
24
src/Database/Tables/Search/Search.php
Normal file
24
src/Database/Tables/Search/Search.php
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VLW\Database\Tables\Search;
|
||||||
|
|
||||||
|
use vlw\xEnum;
|
||||||
|
|
||||||
|
enum SearchCategoryEnum {
|
||||||
|
use xEnum;
|
||||||
|
|
||||||
|
case WORK;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SearchTable: string {
|
||||||
|
use xEnum;
|
||||||
|
|
||||||
|
const NAME = "search";
|
||||||
|
|
||||||
|
case QUERY = "query";
|
||||||
|
case ID = "id";
|
||||||
|
case TITLE = "title";
|
||||||
|
case SUMMARY = "summary";
|
||||||
|
case CATEGORY = "category";
|
||||||
|
case HREF = "href";
|
||||||
|
}
|
|
@ -4,13 +4,13 @@
|
||||||
|
|
||||||
use vlw\xEnum;
|
use vlw\xEnum;
|
||||||
|
|
||||||
enum TagsLabelEnum: string {
|
enum TagsLabelEnum {
|
||||||
use xEnum;
|
use xEnum;
|
||||||
|
|
||||||
case VLW = "VLW";
|
case VLW;
|
||||||
case RELEASE = "RELEASE";
|
case RELEASE;
|
||||||
case WEBSITE = "WEBSITE";
|
case WEBSITE;
|
||||||
case REPO = "REPO";
|
case REPO;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TagsTable: string {
|
enum TagsTable: string {
|
||||||
|
|
Loading…
Add table
Reference in a new issue