Browse Source

Make search more prominent

imwald
Nuša Pukšič 4 months ago
parent
commit
bea0ab9390
  1. 9
      assets/controllers/search_broadcast_controller.js
  2. 12
      assets/controllers/search_visibility_controller.js
  3. 5
      assets/styles/form.css
  4. 2
      assets/styles/utilities.css
  5. 2
      config/packages/security.yaml
  6. 3
      src/Command/NostrEventFromYamlDefinitionCommand.php
  7. 9
      src/Controller/DefaultController.php
  8. 21
      src/Controller/MagazineWizardController.php
  9. 8
      src/Twig/Components/SearchComponent.php
  10. 2
      templates/components/Molecules/Card.html.twig
  11. 18
      templates/components/Organisms/ZineList.html.twig
  12. 29
      templates/components/SearchComponent.html.twig
  13. 5
      templates/components/UserMenu.html.twig
  14. 17
      templates/home.html.twig
  15. 21
      templates/pages/magazine.html.twig
  16. 1
      translations/messages.en.yaml

9
assets/controllers/search_broadcast_controller.js

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
onInput(e) {
const value = e.target.value || '';
const active = value.trim().length > 0;
window.dispatchEvent(new CustomEvent('search:changed', { detail: { active } }));
}
}

12
assets/controllers/search_visibility_controller.js

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['list'];
toggle(event) {
const active = !!event.detail?.active;
if (this.hasListTarget) {
this.listTarget.hidden = active;
}
}
}

5
assets/styles/form.css

@ -67,3 +67,8 @@ textarea, input { @@ -67,3 +67,8 @@ textarea, input {
#editor {
margin: 0;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}

2
assets/styles/utilities.css

@ -45,3 +45,5 @@ @@ -45,3 +45,5 @@
/* Details/Summary tweaks */
details>summary{cursor:pointer}
/* Text truncation with ellipsis */
.line-clamp-5 {display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 5;overflow: hidden;text-overflow: ellipsis;}

2
config/packages/security.yaml

@ -27,6 +27,6 @@ security: @@ -27,6 +27,6 @@ security:
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/admin, roles: ROLE_USER }
- { path: ^/search, roles: ROLE_USER }
# - { path: ^/search, roles: ROLE_USER }
# - { path: ^/nzine, roles: ROLE_USER }
# - { path: ^/profile, roles: ROLE_USER }

3
src/Command/NostrEventFromYamlDefinitionCommand.php

@ -5,6 +5,7 @@ declare(strict_types=1); @@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Command;
use App\Enum\IndexStatusEnum;
use App\Enum\KindsEnum;
use App\Factory\ArticleFactory;
use App\Service\NostrClient;
use Doctrine\ORM\EntityManagerInterface;
@ -71,7 +72,7 @@ class NostrEventFromYamlDefinitionCommand extends Command @@ -71,7 +72,7 @@ class NostrEventFromYamlDefinitionCommand extends Command
try {
// Deserialize YAML content into an Event object
$event = new Event();
$event->setKind(30040);
$event->setKind(KindsEnum::PUBLICATION_INDEX->value);
$tags = $yamlContent['tags'];
$event->setTags($tags);
$items = array_filter($tags, function ($tag) {

9
src/Controller/DefaultController.php

@ -19,6 +19,7 @@ use Psr\Log\LoggerInterface; @@ -19,6 +19,7 @@ use Psr\Log\LoggerInterface;
class DefaultController extends AbstractController
{
public function __construct(
private readonly FinderInterface $finder,
private readonly CacheInterface $redisCache)
{
}
@ -30,6 +31,11 @@ class DefaultController extends AbstractController @@ -30,6 +31,11 @@ class DefaultController extends AbstractController
#[Route('/', name: 'home')]
public function index(): Response
{
// get latest articles
$q = new Query();
$q->setSize(12);
$q->setSort(['createdAt' => ['order' => 'desc']]);
$latest = $this->finder->find($q);
// get newsroom index, loop over categories, pick top three from each and display in sections
$mag = $this->redisCache->get('magazine-newsroom-magazine-by-newsroom', function (){
return null;
@ -41,7 +47,8 @@ class DefaultController extends AbstractController @@ -41,7 +47,8 @@ class DefaultController extends AbstractController
});
return $this->render('home.html.twig', [
'indices' => array_values($cats)
'indices' => array_values($cats),
'latest' => $latest
]);
}

21
src/Controller/MagazineWizardController.php

@ -6,9 +6,11 @@ namespace App\Controller; @@ -6,9 +6,11 @@ namespace App\Controller;
use App\Dto\CategoryDraft;
use App\Dto\MagazineDraft;
use App\Enum\KindsEnum;
use App\Form\CategoryArticlesType;
use App\Form\MagazineSetupType;
use App\Service\RedisCacheService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Cache\CacheItemPoolInterface;
use swentel\nostr\Event\Event;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -164,7 +166,8 @@ class MagazineWizardController extends AbstractController @@ -164,7 +166,8 @@ class MagazineWizardController extends AbstractController
Request $request,
CacheItemPoolInterface $redisCache,
CsrfTokenManagerInterface $csrfTokenManager,
RedisClient $redis
RedisClient $redis,
EntityManagerInterface $entityManager
): JsonResponse {
// Verify CSRF token
$csrfToken = $request->headers->get('X-CSRF-TOKEN');
@ -184,7 +187,7 @@ class MagazineWizardController extends AbstractController @@ -184,7 +187,7 @@ class MagazineWizardController extends AbstractController
$eventObj->setId($signedEvent['id'] ?? '');
$eventObj->setPublicKey($signedEvent['pubkey'] ?? '');
$eventObj->setCreatedAt($signedEvent['created_at'] ?? time());
$eventObj->setKind($signedEvent['kind'] ?? 30040);
$eventObj->setKind($signedEvent['kind'] ?? KindsEnum::PUBLICATION_INDEX->value);
$eventObj->setTags($signedEvent['tags'] ?? []);
$eventObj->setContent($signedEvent['content'] ?? '');
$eventObj->setSignature($signedEvent['sig'] ?? '');
@ -230,6 +233,20 @@ class MagazineWizardController extends AbstractController @@ -230,6 +233,20 @@ class MagazineWizardController extends AbstractController
// non-fatal
}
// Save to persistence as Event entity
// Map swentel Event to Event entity, it's always a new event
$event = new \App\Entity\Event();
$event->setId($eventObj->getId());
$event->setPubkey($eventObj->getPublicKey());
$event->setCreatedAt($eventObj->getCreatedAt());
$event->setKind($eventObj->getKind());
$event->setTags($eventObj->getTags());
$event->setContent($eventObj->getContent());
$event->setSig($eventObj->getSignature());
// Persist
$entityManager->persist($event);
$entityManager->flush();
return new JsonResponse(['ok' => true]);
}

8
src/Twig/Components/SearchComponent.php

@ -28,6 +28,7 @@ final class SearchComponent @@ -28,6 +28,7 @@ final class SearchComponent
public array $results = [];
public bool $interactive = true;
public string $currentRoute;
public int $credits = 0;
public ?string $npub = null;
@ -55,8 +56,9 @@ final class SearchComponent @@ -55,8 +56,9 @@ final class SearchComponent
{
}
public function mount(): void
public function mount($currentRoute = 'search'): void
{
$this->currentRoute = $currentRoute;
$token = $this->tokenStorage->getToken();
$this->npub = $token?->getUserIdentifier();
@ -71,7 +73,7 @@ final class SearchComponent @@ -71,7 +73,7 @@ final class SearchComponent
}
// Restore search results from session if available and no query provided
if (empty($this->query)) {
if (empty($this->query) && $this->currentRoute == 'search') {
$session = $this->requestStack->getSession();
if ($session->has(self::SESSION_QUERY_KEY)) {
$this->query = $session->get(self::SESSION_QUERY_KEY);
@ -237,7 +239,7 @@ final class SearchComponent @@ -237,7 +239,7 @@ final class SearchComponent
]);
// Increase minimum score to filter out irrelevant results
$mainQuery->setMinScore(0.3);
$mainQuery->setMinScore(0.35);
// Sort by score and createdAt
$mainQuery->setSort([

2
templates/components/Molecules/Card.html.twig

@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
<div class="card-body">
<h2 class="card-title">{{ article.title }}</h2>
{% if article.summary %}
<p class="lede">
<p class="lede line-clamp-5">
{{ article.summary }}
</p>
{% endif %}

18
templates/components/Organisms/ZineList.html.twig

@ -1,16 +1,22 @@ @@ -1,16 +1,22 @@
<div {{ attributes }}>
{% for item in nzines %}
{% if item.npub in indices|keys %}
{% set idx = indices[item.npub] %}
{% if idx|length > 0 %}
{% set idx = indices[item.npub] is defined ? indices[item.npub] : null %}
<a class="card" href="{{ path('nzine_view', { npub: item.npub })}}">
<div class="card-body">
<h3 class="card-title">{{ idx.title }}</h3>
<h3 class="card-title">
{% if idx and idx.title %}
{{ idx.title }}
{% elseif item.slug %}
{{ item.slug }}
{% else %}
{{ item.npub }}
{% endif %}
</h3>
{% if idx and idx.summary %}
<p class="hidden">{{ idx.summary }}</p>
{% endif %}
</div>
</a>
<br>
{% endif %}
{% endif %}
{% endfor %}
</div>

29
templates/components/SearchComponent.html.twig

@ -7,16 +7,36 @@ @@ -7,16 +7,36 @@
placeholder="{{ 'text.search'|trans }}"
data-model="norender|query"
value="{{ this.query }}"
data-controller="search-broadcast"
data-action="input->search-broadcast#onInput"
/>
<button type="submit"><twig:ux:icon name="iconoir:search" class="icon" /></button>
<button type="submit"
{% if not is_granted('IS_AUTHENTICATED_FULLY') %}disabled{% endif %}
><twig:ux:icon name="iconoir:search" class="icon" /></button>
</label>
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
<div style="text-align: right">
<small class="help-text">
<em>{{ 'credit.balance'|trans({'%count%': credits, 'count': credits}) }}</em>
</small>
</div>
{% endif %}
</form>
{% if not is_granted('IS_AUTHENTICATED_FULLY') %}
<div class="notice info mb-5">
<p>Log in to search articles.</p>
<small>Currently only available on desktop.</small>
</div>
{% endif %}
{% if is_granted('IS_AUTHENTICATED_FULLY') and credits == 0 %}
<div class="mb-5">
<twig:GetCreditsComponent />
</div>
{% endif %}
<!-- Loading Indicator -->
<div style="text-align: center">
<div class="spinner" data-loading>
@ -31,11 +51,4 @@ @@ -31,11 +51,4 @@
{% elseif this.query is not empty %}
<p><small>{{ 'text.noResults'|trans }}</small></p>
{% endif %}
{% block aside %}
{% if credits == 0 %}
<twig:GetCreditsComponent />
{% endif %}
{% endblock %}
</div>

5
templates/components/UserMenu.html.twig

@ -25,10 +25,9 @@ @@ -25,10 +25,9 @@
</li>
</ul>
{% else %}
<div class="notice info">
<p>Log in to access search.</p>
</div>
<div class="mt-2">
<twig:Atoms:Button {{ ...stimulus_action('login', 'loginAct') }}>{{ 'heading.logIn'|trans }}</twig:Atoms:Button>
</div>
{% endif %}
<div>
<div class="spinner" data-loading>

17
templates/home.html.twig

@ -4,13 +4,18 @@ @@ -4,13 +4,18 @@
{% endblock %}
{% block body %}
{# content #}
{% for item in indices %}
<twig:Organisms:FeaturedList :category="item" class="featured-list"/>
{% endfor %}
<div
data-controller="search-visibility"
data-action="search:changed@window->search-visibility#toggle"
>
<twig:SearchComponent :currentRoute="app.current_route" />
<div data-search-visibility-target="list">
<twig:Organisms:CardList :list="latest" />
</div>
</div>
{% endblock %}
{% block aside %}
{# <h6>Magazines</h6>#}
{# <twig:Organisms:ZineList />#}
<h6>Magazines</h6>
<twig:Organisms:ZineList />
{% endblock %}

21
templates/pages/magazine.html.twig

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
{% extends 'base.html.twig' %}
{% block ogtags %}
<meta property="og:title" content="{{ magazine.title }}">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ app.request.uri }}">
<meta property="og:description" content="{{ magazine.summary }}">
<meta property="og:site_name" content="Newsroom">
{% endblock %}
{% block nav %}
{% endblock %}
{% block body %}
{# hero #}
<twig:Organisms:MagazineHero :magazine="magazine" class="mb-4"/>
{# content #}
{% for item in indices %}
<twig:Organisms:FeaturedList :category="item" class="featured-list"/>
{% endfor %}
{% endblock %}

1
translations/messages.en.yaml

@ -3,6 +3,7 @@ text: @@ -3,6 +3,7 @@ text:
search: 'Search...'
searching: 'Searching...'
noResults: 'No results.'
searchMobileNotice: 'Search is currently only available on desktop for logged-in users.'
heading:
roles: 'Roles'
logout: 'Log out'

Loading…
Cancel
Save