Browse Source

Revise routing, part 6

imwald
Nuša Pukšič 4 months ago
parent
commit
4fb59bed02
  1. 98
      src/Twig/Components/ReadingListDraftComponent.php
  2. 24
      templates/components/ReadingListDraftComponent.html.twig
  3. 6
      templates/components/UserMenu.html.twig
  4. 24
      templates/home.html.twig
  5. 6
      templates/layout.html.twig
  6. 3
      templates/pages/category.html.twig
  7. 14
      templates/reading_list/compose.html.twig
  8. 2
      templates/reading_list/reading_articles.html.twig
  9. 24
      templates/static/landing.html.twig

98
src/Twig/Components/ReadingListDraftComponent.php

@ -3,10 +3,16 @@
namespace App\Twig\Components; namespace App\Twig\Components;
use App\Dto\CategoryDraft; use App\Dto\CategoryDraft;
use App\Enum\KindsEnum;
use App\Service\NostrClient;
use nostriphant\NIP19\Bech32;
use nostriphant\NIP19\Data\NAddr;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveListener; use Symfony\UX\LiveComponent\Attribute\LiveListener;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait; use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent] #[AsLiveComponent]
@ -16,9 +22,20 @@ final class ReadingListDraftComponent
public ?CategoryDraft $draft = null; public ?CategoryDraft $draft = null;
public function __construct(private readonly RequestStack $requestStack) #[LiveProp(writable: true)]
{ public string $naddrInput = '';
}
#[LiveProp]
public string $naddrError = '';
#[LiveProp]
public string $naddrSuccess = '';
public function __construct(
private readonly RequestStack $requestStack,
private readonly NostrClient $nostrClient,
private readonly LoggerInterface $logger,
) {}
public function mount(): void public function mount(): void
{ {
@ -43,6 +60,81 @@ final class ReadingListDraftComponent
} }
} }
#[LiveAction]
public function addNaddr(): void
{
$this->naddrError = '';
$this->naddrSuccess = '';
$raw = trim($this->naddrInput);
if ($raw === '') {
$this->naddrError = 'Empty input.';
return;
}
// Extract naddr (accept nostr:naddr1... or raw naddr1...)
if (preg_match('/(naddr1[0-9a-zA-Z]+)/', $raw, $m) !== 1) {
$this->naddrError = 'No naddr found.';
return;
}
$naddr = $m[1];
try {
$decoded = new Bech32($naddr);
if ($decoded->type !== 'naddr') {
$this->naddrError = 'Invalid naddr type.';
return;
}
/** @var NAddr $data */
$data = $decoded->data;
$slug = $data->identifier;
$pubkey = $data->pubkey;
$kind = $data->kind;
$relays = $data->relays;
if ($kind !== KindsEnum::LONGFORM->value) {
$this->naddrError = 'Not a long-form article (kind '.$kind.').';
return;
}
if (!$slug) {
$this->naddrError = 'Missing identifier (slug).';
return;
}
$coordinate = $kind . ':' . $pubkey . ':' . $slug;
$session = $this->requestStack->getSession();
$draft = $session->get('read_wizard');
if (!$draft instanceof CategoryDraft) {
$draft = new CategoryDraft();
}
if (!in_array($coordinate, $draft->articles, true)) {
// Attempt to fetch article so it exists locally (best-effort)
try {
$this->nostrClient->getLongFormFromNaddr($slug, $relays, $pubkey, $kind);
} catch (\Throwable $e) {
// Non-fatal; still add coordinate
$this->logger->warning('Failed fetching article from naddr', [
'error' => $e->getMessage(),
'naddr' => $naddr
]);
}
$draft->articles[] = $coordinate;
$session->set('read_wizard', $draft);
$this->draft = $draft;
$this->naddrSuccess = 'Added article: ' . $coordinate;
} else {
$this->naddrSuccess = 'Article already in list.';
}
$this->naddrInput = '';
} catch (\Throwable $e) {
$this->naddrError = 'Decode failed.';
$this->logger->error('naddr decode failed', [
'input' => $raw,
'error' => $e->getMessage()
]);
}
}
private function reloadFromSession(): void private function reloadFromSession(): void
{ {
$session = $this->requestStack->getSession(); $session = $this->requestStack->getSession();

24
templates/components/ReadingListDraftComponent.html.twig

@ -4,7 +4,7 @@
{% if draft.summary %}<p>{{ draft.summary }}</p>{% endif %} {% if draft.summary %}<p>{{ draft.summary }}</p>{% endif %}
<h3>Articles</h3> <h3 class="mt-4">Articles</h3>
{% if draft.articles is not empty %} {% if draft.articles is not empty %}
<ul class="small"> <ul class="small">
{% for coord in draft.articles %} {% for coord in draft.articles %}
@ -15,11 +15,29 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
<p><small>No articles yet. Use search to add some.</small></p> <p><small>No articles yet. Use search or paste an naddr to add some.</small></p>
{% endif %} {% endif %}
<div class="mt-3">
<form class="d-flex gap-3" data-action="live#action:prevent" data-live-action-param="addNaddr">
<label>
<input
type="text"
placeholder="Paste article naddr (nostr:naddr1...)"
class="form-control form-control-sm"
data-model="norender|naddrInput"
value="{{ naddrInput }}"
/>
</label>
<div class="actions">
<button class="btn btn-sm btn-secondary">Add</button>
</div>
</form>
{% if naddrError %}<div class="small text-danger mt-1">{{ naddrError }}</div>{% endif %}
{% if naddrSuccess %}<div class="small text-success mt-1">{{ naddrSuccess }}</div>{% endif %}
</div>
<div class="mt-3"> <div class="mt-3">
<a class="btn btn-primary" href="{{ path('read_wizard_review') }}">Review & Sign</a> <a class="btn btn-primary" href="{{ path('read_wizard_review') }}">Review & Sign</a>
</div> </div>
</div> </div>

6
templates/components/UserMenu.html.twig

@ -14,9 +14,9 @@
<li> <li>
<a href="{{ path('editor-create') }}">Write Article</a> <a href="{{ path('editor-create') }}">Write Article</a>
</li> </li>
<li> {# <li>#}
<a href="{{ path('reading_list_index') }}">Compose List</a> {# <a href="{{ path('reading_list_index') }}">Compose List</a>#}
</li> {# </li>#}
{% if is_granted('ROLE_ADMIN') %} {% if is_granted('ROLE_ADMIN') %}
<li> <li>
<a href="{{ path('mag_wizard_setup') }}">Create Magazine</a> <a href="{{ path('mag_wizard_setup') }}">Create Magazine</a>

24
templates/home.html.twig

@ -36,18 +36,18 @@
</div> </div>
</section> </section>
<section class="d-flex gap-3 center ln-section--lists"> {# <section class="d-flex gap-3 center ln-section--lists">#}
<div class="container mt-5"> {# <div class="container mt-5">#}
<h1>Reading Lists</h1> {# <h1>Reading Lists</h1>#}
<p class="eyebrow">for collections, curations, courses and more</p> {# <p class="eyebrow">for collections, curations, courses and more</p>#}
</div> {# </div>#}
<div class="mb-5"> {# <div class="mb-5">#}
<p class="measure">Create ordered reading lists. Add more articles, reorder, republish.</p> {# <p class="measure">Create ordered reading lists. Add more articles, reorder, republish.</p>#}
<div class="cta-row"> {# <div class="cta-row">#}
<a class="btn btn--primary" href="{{ path('reading_list_index') }}">Start a list</a> {# <a class="btn btn--primary" href="{{ path('reading_list_index') }}">Start a list</a>#}
</div> {# </div>#}
</div> {# </div>#}
</section> {# </section>#}
<section class="d-flex gap-3 center ln-section--editor"> <section class="d-flex gap-3 center ln-section--editor">
<div class="container mt-5"> <div class="container mt-5">

6
templates/layout.html.twig

@ -11,9 +11,9 @@
<li> <li>
<a href="{{ path('newsstand') }}">Newsstand</a> <a href="{{ path('newsstand') }}">Newsstand</a>
</li> </li>
<li> {# <li>#}
<a href="{{ path('lists') }}">Lists</a> {# <a href="{{ path('lists') }}">Lists</a>#}
</li> {# </li>#}
<li> <li>
<a href="{{ path('app_search_index') }}">{{ 'heading.search'|trans }}</a> <a href="{{ path('app_search_index') }}">{{ 'heading.search'|trans }}</a>
</li> </li>

3
templates/pages/category.html.twig

@ -10,6 +10,7 @@
{% block body %} {% block body %}
<twig:Organisms:MagazineHero :mag="mag" :magazine="magazine" /> <twig:Organisms:MagazineHero :mag="mag" :magazine="magazine" />
<div class="w-container">
<twig:Organisms:CardList :list="list" :category="index" :mag="mag" class="article-list" /> <twig:Organisms:CardList :list="list" :category="index" :mag="mag" class="article-list" />
</div>
{% endblock %} {% endblock %}

14
templates/reading_list/compose.html.twig

@ -1,17 +1,17 @@
{% extends 'layout.html.twig' %} {% extends 'layout.html.twig' %}
{% block body %} {% block body %}
<section class="d-flex gap-3 center ln-section--newsstand">
<div class="container mt-3 mb-3">
<h1>Compose Reading List</h1> <h1>Compose Reading List</h1>
</div>
<section>
<twig:ReadingListDraftComponent />
</section> </section>
{% endblock %}
{% block aside %} <section class="d-flex flex-row gap-3">
<div class="mt-2"> <twig:ReadingListDraftComponent />
<h1>Search & Add</h1> <div class="mt-3">
<p>Search articles and click “Add to list”.</p> <p>Search articles and click “Add to list”.</p>
<twig:SearchComponent :selectMode="true" currentRoute="compose" /> <twig:SearchComponent :selectMode="true" currentRoute="compose" />
</div> </div>
</section>
{% endblock %} {% endblock %}

2
templates/reading_list/reading_articles.html.twig

@ -23,7 +23,7 @@
<button type="button" class="btn btn-secondary mt-2" {{ stimulus_action('form-collection', 'addCollectionElement') }}>Add article</button> <button type="button" class="btn btn-secondary mt-2" {{ stimulus_action('form-collection', 'addCollectionElement') }}>Add article</button>
</div> </div>
<div class="mt-3 d-flex flex-row gap-2"> <div class="mt-3 d-flex flex-row gap-2 actions">
<a class="btn btn-secondary" href="{{ path('read_wizard_setup') }}">Back</a> <a class="btn btn-secondary" href="{{ path('read_wizard_setup') }}">Back</a>
<a class="btn btn-secondary" href="{{ path('read_wizard_cancel') }}">Cancel</a> <a class="btn btn-secondary" href="{{ path('read_wizard_cancel') }}">Cancel</a>
<button class="btn btn-primary">Review</button> <button class="btn btn-primary">Review</button>

24
templates/static/landing.html.twig

@ -36,18 +36,18 @@
</div> </div>
</section> </section>
<section class="d-flex gap-3 center ln-section--lists"> {# <section class="d-flex gap-3 center ln-section--lists">#}
<div class="container mt-5"> {# <div class="container mt-5">#}
<h1>Reading Lists</h1> {# <h1>Reading Lists</h1>#}
<p class="eyebrow">for collections, curations, courses and more</p> {# <p class="eyebrow">for collections, curations, courses and more</p>#}
</div> {# </div>#}
<div class="mb-5"> {# <div class="mb-5">#}
<p class="measure">Create ordered reading lists. Add more articles, reorder, republish.</p> {# <p class="measure">Create ordered reading lists. Add more articles, reorder, republish.</p>#}
<div class="cta-row"> {# <div class="cta-row">#}
<a class="btn btn--primary" href="{{ path('reading_list_index') }}">Browse</a> {# <a class="btn btn--primary" href="{{ path('reading_list_index') }}">Browse</a>#}
</div> {# </div>#}
</div> {# </div>#}
</section> {# </section>#}
<section class="d-flex gap-3 center ln-section--editor"> <section class="d-flex gap-3 center ln-section--editor">
<div class="container mt-5"> <div class="container mt-5">

Loading…
Cancel
Save