16 changed files with 267 additions and 291 deletions
@ -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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
<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