16 changed files with 267 additions and 291 deletions
@ -1,127 +0,0 @@
@@ -1,127 +0,0 @@
|
||||
<?php |
||||
|
||||
namespace App\Twig\Components; |
||||
|
||||
use App\Repository\ArticleRepository; |
||||
use Psr\Log\LoggerInterface; |
||||
use Symfony\Component\HttpFoundation\RequestStack; |
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; |
||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; |
||||
use Symfony\UX\LiveComponent\Attribute\LiveAction; |
||||
use Symfony\UX\LiveComponent\Attribute\LiveProp; |
||||
use Symfony\UX\LiveComponent\DefaultActionTrait; |
||||
use Symfony\Contracts\Cache\CacheInterface; |
||||
|
||||
#[AsLiveComponent] |
||||
final class SearchComponent |
||||
{ |
||||
use DefaultActionTrait; |
||||
|
||||
#[LiveProp(writable: true, useSerializerForHydration: true)] |
||||
public string $query = ''; |
||||
public array $results = []; |
||||
|
||||
public bool $interactive = true; |
||||
|
||||
#[LiveProp] |
||||
public int $vol = 0; |
||||
|
||||
#[LiveProp(writable: true)] |
||||
public int $page = 1; |
||||
|
||||
#[LiveProp] |
||||
public int $resultsPerPage = 12; |
||||
|
||||
private const SESSION_KEY = 'last_search_results'; |
||||
private const SESSION_QUERY_KEY = 'last_search_query'; |
||||
|
||||
public function __construct( |
||||
private readonly ArticleRepository $articleRepository, |
||||
private readonly TokenStorageInterface $tokenStorage, |
||||
private readonly LoggerInterface $logger, |
||||
private readonly CacheInterface $cache, |
||||
private readonly RequestStack $requestStack |
||||
) |
||||
{ |
||||
} |
||||
|
||||
public function mount(): void |
||||
{ |
||||
// Restore search results from session if available and no query provided |
||||
if (empty($this->query)) { |
||||
$session = $this->requestStack->getSession(); |
||||
if ($session->has(self::SESSION_QUERY_KEY)) { |
||||
$this->query = $session->get(self::SESSION_QUERY_KEY); |
||||
$this->results = $session->get(self::SESSION_KEY, []); |
||||
$this->logger->info('Restored search results from session for query: ' . $this->query); |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[LiveAction] |
||||
public function search(): void |
||||
{ |
||||
$this->logger->info("Query: {$this->query}"); |
||||
|
||||
if (empty($this->query)) { |
||||
$this->results = []; |
||||
$this->clearSearchCache(); |
||||
return; |
||||
} |
||||
|
||||
// Check if the same query exists in session |
||||
$session = $this->requestStack->getSession(); |
||||
if ($session->has(self::SESSION_QUERY_KEY) && |
||||
$session->get(self::SESSION_QUERY_KEY) === $this->query) { |
||||
$this->results = $session->get(self::SESSION_KEY, []); |
||||
$this->logger->info('Using cached search results for query: ' . $this->query); |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
$this->results = []; |
||||
|
||||
// Use database-based search instead of Elasticsearch |
||||
$offset = ($this->page - 1) * $this->resultsPerPage; |
||||
$results = $this->articleRepository->searchArticles( |
||||
$this->query, |
||||
$this->resultsPerPage, |
||||
$offset |
||||
); |
||||
|
||||
$this->logger->info('Search results count: ' . count($results)); |
||||
$this->logger->info('Search results: ', ['results' => $results]); |
||||
|
||||
$this->results = $results; |
||||
|
||||
// Cache the search results in session |
||||
$this->saveSearchToSession($this->query, $this->results); |
||||
|
||||
} catch (\Exception $e) { |
||||
$this->logger->error('Search error: ' . $e->getMessage()); |
||||
$this->results = []; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Save search results to session |
||||
*/ |
||||
private function saveSearchToSession(string $query, array $results): void |
||||
{ |
||||
$session = $this->requestStack->getSession(); |
||||
$session->set(self::SESSION_QUERY_KEY, $query); |
||||
$session->set(self::SESSION_KEY, $results); |
||||
$this->logger->info('Saved search results to session for query: ' . $query); |
||||
} |
||||
|
||||
/** |
||||
* Clear search cache from session |
||||
*/ |
||||
private function clearSearchCache(): void |
||||
{ |
||||
$session = $this->requestStack->getSession(); |
||||
$session->remove(self::SESSION_QUERY_KEY); |
||||
$session->remove(self::SESSION_KEY); |
||||
$this->logger->info('Cleared search cache from session'); |
||||
} |
||||
} |
||||
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
{% set ctx = comment_reply_context|default(null) %} |
||||
{% if ctx and ctx.can_publish|default(false) %} |
||||
{% for row in ctx.rows|default([]) %} |
||||
{% if row.mode|default('') == 'article' %} |
||||
<div |
||||
class="comment-reply comment-reply--article card" |
||||
data-controller="comment-reply" |
||||
data-comment-reply-publish-url-value="{{ path('comment_reply_publish')|e('html_attr') }}" |
||||
data-comment-reply-csrf-value="{{ csrf_token('comment_reply')|e('html_attr') }}" |
||||
data-comment-reply-expected-coordinate-value="{{ ctx.coordinate|e('html_attr') }}" |
||||
data-comment-reply-article-event-id-value="{{ ctx.article_event_id|default('')|e('html_attr') }}" |
||||
data-comment-reply-fragment-url-value="{{ ctx.fragment_url|default('')|e('html_attr') }}" |
||||
data-comment-reply-refresh-after-value="1" |
||||
data-comment-reply-expected-tags-value='{{ row.expectedTags|default([])|json_encode|e('html_attr') }}' |
||||
data-comment-reply-parent-kind-value="{{ row.parentKind|default(0) }}" |
||||
data-comment-reply-parent-id-value="{{ row.parentId|default('')|e('html_attr') }}" |
||||
data-comment-reply-author-pubkey-value="{{ row.authorPubkey|default('')|e('html_attr') }}" |
||||
> |
||||
<div class="card-body comment-reply--article__inner"> |
||||
<div class="comment-reply__toolbar"> |
||||
<p class="comment-reply__lede text-subtle">Reply to this note on Nostr (kind 1111).</p> |
||||
<button |
||||
type="button" |
||||
class="btn btn-secondary btn-sm comment-reply__toggle" |
||||
data-comment-reply-target="toggleBtn" |
||||
data-action="click->comment-reply#togglePanel" |
||||
aria-expanded="false" |
||||
>Reply</button> |
||||
</div> |
||||
<div class="comment-reply__panel comment-reply__panel--hidden" data-comment-reply-target="panel"> |
||||
<form |
||||
class="comment-reply__form" |
||||
data-action="submit->comment-reply#publish" |
||||
> |
||||
<div class="comment-reply__body"> |
||||
<label class="visually-hidden" for="comment-reply-article-body">Your reply</label> |
||||
<textarea |
||||
class="form-control" |
||||
id="comment-reply-article-body" |
||||
name="body" |
||||
rows="4" |
||||
required |
||||
minlength="1" |
||||
placeholder="Write a NIP-22 comment (kind 1111)." |
||||
></textarea> |
||||
</div> |
||||
<div class="comment-reply__actions"> |
||||
<button class="btn btn-primary" type="submit">Sign & publish</button> |
||||
</div> |
||||
<p class="comment-reply__hint text-subtle" data-comment-reply-target="hint" aria-live="polite"></p> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% endif %} |
||||
{% endfor %} |
||||
{% endif %} |
||||
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
{% set _page = page|default(1) %} |
||||
{% set _last = last_page|default(1) %} |
||||
{% if _last > 1 %} |
||||
<nav class="pager" aria-label="{{ aria_label|default('Pagination') }}"> |
||||
<div class="pager__inner"> |
||||
{% if prev_url is not empty %} |
||||
<a class="btn btn-outline-secondary pager__btn" href="{{ prev_url }}">Newer</a> |
||||
{% else %} |
||||
<span class="btn btn-outline-secondary pager__btn is-disabled" aria-disabled="true">Newer</span> |
||||
{% endif %} |
||||
<span class="pager__status text-subtle">Page {{ _page }} of {{ _last }}</span> |
||||
{% if next_url is not empty %} |
||||
<a class="btn btn-outline-secondary pager__btn" href="{{ next_url }}">Older</a> |
||||
{% else %} |
||||
<span class="btn btn-outline-secondary pager__btn is-disabled" aria-disabled="true">Older</span> |
||||
{% endif %} |
||||
</div> |
||||
</nav> |
||||
{% endif %} |
||||
@ -1,29 +0,0 @@
@@ -1,29 +0,0 @@
|
||||
<div {{ attributes }}> |
||||
{% if interactive %} |
||||
<form data-live-action-param="search" |
||||
data-action="live#action:prevent"> |
||||
<label class="search"> |
||||
<input type="search" |
||||
placeholder="{{ 'text.search'|trans }}" |
||||
data-model="norender|query" |
||||
value="{{ this.query }}" |
||||
/> |
||||
<button type="submit"><twig:ux:icon name="iconoir:search" class="icon" /></button> |
||||
</label> |
||||
</form> |
||||
|
||||
<!-- Loading Indicator --> |
||||
<div style="text-align: center"> |
||||
<div class="spinner" data-loading> |
||||
<div class="lds-dual-ring"></div> |
||||
</div> |
||||
</div> |
||||
{% endif %} |
||||
|
||||
<!-- Results --> |
||||
{% if this.results is not empty %} |
||||
<twig:Organisms:CardList :list="this.results" class="article-list" /> |
||||
{% elseif this.query is not empty %} |
||||
<p><small>{{ 'text.noResults'|trans }}</small></p> |
||||
{% endif %} |
||||
</div> |
||||
Loading…
Reference in new issue