Browse Source

Curated lists

imwald
Nuša Pukšič 4 months ago
parent
commit
111b54843a
  1. 57
      src/Controller/ReadingListController.php
  2. 59
      src/Twig/Components/ReadingListList.php
  3. 16
      templates/components/Organisms/ReadingListList.html.twig
  4. 4
      templates/components/Organisms/ZineList.html.twig
  5. 3
      templates/home.html.twig
  6. 2
      templates/pages/category.html.twig
  7. 32
      templates/reading_list/index.html.twig

57
src/Controller/ReadingListController.php

@ -4,6 +4,9 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use App\Entity\Event;
use Doctrine\ORM\EntityManagerInterface;
use swentel\nostr\Key\Key;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@ -11,9 +14,59 @@ use Symfony\Component\Routing\Attribute\Route;
class ReadingListController extends AbstractController class ReadingListController extends AbstractController
{ {
#[Route('/reading-list', name: 'reading_list_index')] #[Route('/reading-list', name: 'reading_list_index')]
public function index(): Response public function index(EntityManagerInterface $em): Response
{ {
return $this->render('reading_list/index.html.twig'); $lists = [];
$user = $this->getUser();
$pubkeyHex = null;
if ($user && method_exists($user, 'getUserIdentifier')) {
try {
$key = new Key();
$pubkeyHex = $key->convertToHex($user->getUserIdentifier());
} catch (\Throwable $e) {
$pubkeyHex = null;
}
}
if ($pubkeyHex) {
$repo = $em->getRepository(Event::class);
$events = $repo->findBy(['kind' => 30040, 'pubkey' => $pubkeyHex], ['created_at' => 'DESC']);
$seenSlugs = [];
foreach ($events as $ev) {
if (!$ev instanceof Event) continue;
$tags = $ev->getTags();
$isReadingList = false;
$title = null; $slug = null; $summary = null;
foreach ($tags as $t) {
if (is_array($t)) {
if (($t[0] ?? null) === 'type' && ($t[1] ?? null) === 'reading-list') { $isReadingList = true; }
if (($t[0] ?? null) === 'title') { $title = (string)$t[1]; }
if (($t[0] ?? null) === 'summary') { $summary = (string)$t[1]; }
if (($t[0] ?? null) === 'd') { $slug = (string)$t[1]; }
}
}
if ($isReadingList) {
// Collapse by slug: keep only newest per slug
$keySlug = $slug ?: ('__no_slug__:' . $ev->getId());
if (isset($seenSlugs[$slug ?? $keySlug])) {
continue;
}
$seenSlugs[$slug ?? $keySlug] = true;
$lists[] = [
'id' => $ev->getId(),
'title' => $title ?: '(untitled)',
'summary' => $summary,
'slug' => $slug,
'createdAt' => $ev->getCreatedAt(),
];
}
}
}
return $this->render('reading_list/index.html.twig', [
'lists' => $lists,
]);
} }
#[Route('/reading-list/compose', name: 'reading_list_compose')] #[Route('/reading-list/compose', name: 'reading_list_compose')]

59
src/Twig/Components/ReadingListList.php

@ -0,0 +1,59 @@
<?php
namespace App\Twig\Components;
use App\Entity\Event;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent('Organisms:ReadingListList')]
final class ReadingListList
{
public int $limit = 10;
public function __construct(private readonly EntityManagerInterface $em)
{
}
/**
* @return array<int, array{title:string, slug:string, createdAt:int, pubkey:string}>
*/
public function getLists(): array
{
$repo = $this->em->getRepository(Event::class);
// Fetch more than we need to allow collapsing by slug
/** @var Event[] $events */
$events = $repo->findBy(['kind' => 30040], ['created_at' => 'DESC'], 200);
$out = [];
$seen = [];
foreach ($events as $ev) {
$tags = $ev->getTags();
$isReadingList = false;
$title = null; $slug = null;
foreach ($tags as $t) {
if (!is_array($t)) continue;
if (($t[0] ?? null) === 'type' && ($t[1] ?? null) === 'reading-list') { $isReadingList = true; }
if (($t[0] ?? null) === 'title') { $title = (string)$t[1]; }
if (($t[0] ?? null) === 'd') { $slug = (string)$t[1]; }
}
if (!$isReadingList) continue;
// Require slug; skip malformed events without slug
if (!$slug) continue;
// Collapse newest by slug
if (isset($seen[$slug])) continue;
$seen[$slug] = true;
$out[] = [
'title' => $title ?: '(untitled)',
'slug' => $slug,
'createdAt' => $ev->getCreatedAt(),
'pubkey' => $ev->getPubkey(),
];
if (count($out) >= $this->limit) break;
}
return $out;
}
}

16
templates/components/Organisms/ReadingListList.html.twig

@ -0,0 +1,16 @@
<div {{ attributes }}>
{% set items = this.lists %}
{% if items is not empty %}
<ul class="list-unstyled small d-grid gap-2">
{% for item in items %}
<li>
<a href="{{ path('magazine-category', { slug: item.slug }) }}">{{ item.title }}</a>
<div><small class="text-muted">{{ item.createdAt|date('Y-m-d') }}</small></div>
</li>
{% endfor %}
</ul>
{% else %}
<p><small>No reading lists yet.</small></p>
{% endif %}
</div>

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

@ -1,4 +1,5 @@
<div {{ attributes }}> <div {{ attributes }}>
{% if nzines is not empty %}
{% for item in nzines %} {% for item in nzines %}
{% set idx = indices[item.npub] is defined ? indices[item.npub] : null %} {% set idx = indices[item.npub] is defined ? indices[item.npub] : null %}
<a class="card" href="{{ path('nzine_view', { npub: item.npub })}}"> <a class="card" href="{{ path('nzine_view', { npub: item.npub })}}">
@ -19,4 +20,7 @@
</a> </a>
<br> <br>
{% endfor %} {% endfor %}
{% else %}
<p><small>No magazines yet.</small></p>
{% endif %}
</div> </div>

3
templates/home.html.twig

@ -18,4 +18,7 @@
{% block aside %} {% block aside %}
<h6>Magazines</h6> <h6>Magazines</h6>
<twig:Organisms:ZineList /> <twig:Organisms:ZineList />
<h6>Lists</h6>
<twig:Organisms:ReadingListList />
{% endblock %} {% endblock %}

2
templates/pages/category.html.twig

@ -4,7 +4,7 @@
<meta property="og:title" content="{{ category.title }} - Newsroom"> <meta property="og:title" content="{{ category.title }} - Newsroom">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="{{ app.request.uri }}"> <meta property="og:url" content="{{ app.request.uri }}">
<meta property="og:description" content="{{ category.summary }}"> <meta property="og:description" content="{{ category.summary ?? '' }}">
<meta property="og:site_name" content="Newsroom"> <meta property="og:site_name" content="Newsroom">
{% endblock %} {% endblock %}

32
templates/reading_list/index.html.twig

@ -3,8 +3,38 @@
{% block body %} {% block body %}
<h1>Your Reading Lists</h1> <h1>Your Reading Lists</h1>
<p>Create and share curated reading lists.</p> <p>Create and share curated reading lists.</p>
{% if lists is defined and lists|length %}
<ul class="list-unstyled d-grid gap-2 mb-4">
{% for item in lists %}
<li class="card p-3">
<div class="d-flex justify-content-between align-items-start gap-3">
<div class="flex-fill">
<h3 class="h5 m-0">{{ item.title }}</h3>
{% if item.summary %}<p class="small mt-1 mb-0">{{ item.summary }}</p>{% endif %}
<small class="text-muted">slug: {{ item.slug ?: '—' }} • created: {{ item.createdAt|date('Y-m-d H:i') }}</small>
</div>
<div class="d-flex flex-row gap-2">
<a class="btn btn-sm btn-primary" href="{{ path('reading_list_compose') }}">Open Composer</a>
{% if item.slug %}
<a class="btn btn-sm btn-outline-primary" href="{{ path('magazine-category', { slug: item.slug }) }}">View</a>
<span data-controller="copy-to-clipboard">
<span class="hidden" data-copy-to-clipboard-target="textToCopy">{{ absolute_url(path('magazine-category', { slug: item.slug })) }}</span>
<button class="btn btn-sm btn-secondary"
data-copy-to-clipboard-target="copyButton"
data-action="click->copy-to-clipboard#copyToClipboard">Copy link</button>
</span>
{% endif %}
</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p><small>No reading lists found.</small></p>
{% endif %}
<div class="d-flex flex-row gap-2"> <div class="d-flex flex-row gap-2">
<a class="btn btn-primary" href="{{ path('read_wizard_setup') }}">Create a Reading List</a> <a class="btn btn-primary" href="{{ path('read_wizard_setup') }}">Create a Reading List</a>
<a class="btn btn-secondary" href="{{ path('reading_list_compose') }}">Open Composer</a>
</div> </div>
{% endblock %} {% endblock %}

Loading…
Cancel
Save