Browse Source

Share

imwald
Nuša Pukšič 3 months ago
parent
commit
913b962a7d
  1. 33
      assets/controllers/share_dropdown_controller.js
  2. 1
      assets/styles/03-components/image-upload.css
  3. 1
      config/packages/twig.yaml
  4. 14
      src/Controller/ArticleController.php
  5. 2
      src/Entity/Article.php
  6. 24
      src/Twig/Filters.php
  7. 2
      templates/feedback/form.html.twig
  8. 82
      templates/pages/article.html.twig
  9. 39
      templates/pages/editor.html.twig

33
assets/controllers/share_dropdown_controller.js

@ -0,0 +1,33 @@
// assets/controllers/share_dropdown_controller.js
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['menu', 'button'];
connect() {
document.addEventListener('click', this.closeMenu);
}
disconnect() {
document.removeEventListener('click', this.closeMenu);
}
toggle(event) {
event.stopPropagation();
this.menuTarget.style.display = this.menuTarget.style.display === 'block' ? 'none' : 'block';
}
closeMenu = () => {
this.menuTarget.style.display = 'none';
}
copy(event) {
const el = event.currentTarget;
const text = el.dataset.copy;
navigator.clipboard.writeText(text).then(() => {
const orig = el.innerHTML;
el.innerHTML = 'Copied!';
setTimeout(() => { el.innerHTML = orig; }, 1200);
});
}
}

1
assets/styles/03-components/image-upload.css

@ -23,7 +23,6 @@
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
background-color: var(--color-bg, #fff); background-color: var(--color-bg, #fff);
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.3); box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.3);
max-width: 90%; max-width: 90%;
width: 500px; width: 500px;

1
config/packages/twig.yaml

@ -3,7 +3,6 @@ twig:
globals: globals:
project_npub: 'npub1ez09adke4vy8udk3y2skwst8q5chjgqzym9lpq4u58zf96zcl7kqyry2lz' project_npub: 'npub1ez09adke4vy8udk3y2skwst8q5chjgqzym9lpq4u58zf96zcl7kqyry2lz'
dev_npub: 'npub1636uujeewag8zv8593lcvdrwlymgqre6uax4anuq3y5qehqey05sl8qpl4' dev_npub: 'npub1636uujeewag8zv8593lcvdrwlymgqre6uax4anuq3y5qehqey05sl8qpl4'
feature_flag_share_btn: false
mercure_public_hub_url: '%mercure_public_hub_url%' mercure_public_hub_url: '%mercure_public_hub_url%'
when@test: when@test:

14
src/Controller/ArticleController.php

@ -160,11 +160,19 @@ class ArticleController extends AbstractController
$article = array_shift($articles); $article = array_shift($articles);
} }
if ($article->getPubkey() === null) { $recentArticles = [];
$drafts = [];
$user = $this->getUser(); $user = $this->getUser();
$key = new Key();
if (!!$user) { if (!!$user) {
$key = new Key();
$currentPubkey = $key->convertToHex($user->getUserIdentifier()); $currentPubkey = $key->convertToHex($user->getUserIdentifier());
$recentArticles = $entityManager->getRepository(Article::class)
->findBy(['pubkey' => $currentPubkey, 'kind' => KindsEnum::LONGFORM], ['createdAt' => 'DESC'], 5);
$drafts = $entityManager->getRepository(Article::class)
->findBy(['pubkey' => $currentPubkey, 'kind' => KindsEnum::LONGFORM_DRAFT], ['createdAt' => 'DESC'], 5);
if ($article->getPubkey() === null) {
$article->setPubkey($currentPubkey); $article->setPubkey($currentPubkey);
} }
} }
@ -176,6 +184,8 @@ class ArticleController extends AbstractController
return $this->render('pages/editor.html.twig', [ return $this->render('pages/editor.html.twig', [
'article' => $article, 'article' => $article,
'form' => $form->createView(), 'form' => $form->createView(),
'recentArticles' => $recentArticles,
'drafts' => $drafts,
]); ]);
} }

2
src/Entity/Article.php

@ -328,7 +328,7 @@ class Article
return $this->eventStatus === EventStatusEnum::PREVIEW; return $this->eventStatus === EventStatusEnum::PREVIEW;
} }
public function getRaw() public function getRaw(): ?array
{ {
return $this->raw; return $this->raw;
} }

24
src/Twig/Filters.php

@ -4,8 +4,16 @@ declare(strict_types=1);
namespace App\Twig; namespace App\Twig;
use App\Entity\Article;
use App\Entity\Event as EventEntity;
use BitWasp\Bech32\Exception\Bech32Exception; use BitWasp\Bech32\Exception\Bech32Exception;
use Exception;
use swentel\nostr\Event\Event;
use swentel\nostr\Nip19\Nip19Helper; use swentel\nostr\Nip19\Nip19Helper;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\TwigFilter; use Twig\TwigFilter;
@ -18,6 +26,7 @@ class Filters extends AbstractExtension
new TwigFilter('linkify', [$this, 'linkify'], ['is_safe' => ['html']]), new TwigFilter('linkify', [$this, 'linkify'], ['is_safe' => ['html']]),
new TwigFilter('mentionify', [$this, 'mentionify'], ['is_safe' => ['html']]), new TwigFilter('mentionify', [$this, 'mentionify'], ['is_safe' => ['html']]),
new TwigFilter('nEncode', [$this, 'nEncode']), new TwigFilter('nEncode', [$this, 'nEncode']),
new TwigFilter('naddrEncode', [$this, 'naddrEncode']),
]; ];
} }
@ -71,4 +80,19 @@ class Filters extends AbstractExtension
return $eventId; // Return original if encoding fails return $eventId; // Return original if encoding fails
} }
} }
/**
* @throws Bech32Exception
* @throws Exception
*/
public function naddrEncode(Article $article): string
{
$nip19 = new Nip19Helper();
if ($article->getRaw() !== null) {
$event = Event::fromVerified((object)$article->getRaw() ?? '');
return $nip19->encodeAddr($event, $article->getSlug(), $article->getKind()->value);
} else {
return $nip19->encodeNote($article->getEventId());
}
}
} }

2
templates/feedback/form.html.twig

@ -3,7 +3,7 @@
{% block body %} {% block body %}
<div class="w-container mt-5"> <div class="w-container mt-5">
<twig:Atoms:PageHeading heading="Feedback" tagline="Bug reports and feature requests welcome"/> <twig:Atoms:PageHeading heading="Feedback" tagline="Bug reports and feature requests"/>
<section> <section>
<form data-controller="nostr-single-sign" data-nostr-single-sign-event-value='{{ { <form data-controller="nostr-single-sign" data-nostr-single-sign-event-value='{{ {
"kind": 24, "kind": 24,

82
templates/pages/article.html.twig

@ -16,7 +16,42 @@
<twig:Organisms:MagazineHero :mag="mag" :magazine="magazine" /> <twig:Organisms:MagazineHero :mag="mag" :magazine="magazine" />
{% endif %} {% endif %}
<article class="w-container">
<div class="article-actions"> <div class="article-actions">
<div data-controller="share-dropdown" class="dropdown share-dropdown" style="display:inline-block;position:relative;">
<button data-share-dropdown-target="button"
class="btn btn-secondary"
id="shareBtn"
type="button"
aria-haspopup="true"
aria-expanded="false"
data-action="click->share-dropdown#toggle">
Share
</button>
<div data-share-dropdown-target="menu"
class="dropdown-menu"
id="shareDropdown"
style="display:none;position:absolute;z-index:1000;min-width:200px;background:#fff;border:1px solid #ccc;padding:0.5em 0;box-shadow:0 2px 8px rgba(0,0,0,0.1);">
<button class="dropdown-item"
type="button"
data-action="click->share-dropdown#copy"
data-copy="{{ canonical|e('js') }}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="vertical-align:middle;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h6m0 0v6m0-6L10 19l-7-7" />
</svg>
Newsroom Link
</button>
<button class="dropdown-item"
type="button"
data-action="click->share-dropdown#copy"
data-copy="{{ article|naddrEncode }}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="vertical-align:middle;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12H9m12 0A9 9 0 11 3 12a9 9 0 0118 0z" />
</svg>
Naddr
</button>
</div>
</div>
{% if canEdit %} {% if canEdit %}
<a class="btn btn-primary" href="{{ path('editor-edit-slug', {'slug': article.slug}) }}">Edit article</a> <a class="btn btn-primary" href="{{ path('editor-edit-slug', {'slug': article.slug}) }}">Edit article</a>
{% endif %} {% endif %}
@ -30,54 +65,9 @@
Copy coordinates Copy coordinates
</button> </button>
{% endif %} {% endif %}
<!-- Share Button Dropdown -->
{% if feature_flag_share_btn %}
<div class="dropdown share-dropdown" style="display:inline-block;position:relative;">
<button class="btn btn-secondary" id="shareBtn" type="button" aria-haspopup="true" aria-expanded="false">
Share
</button>
<div class="dropdown-menu" id="shareDropdown" style="display:none;position:absolute;z-index:1000;min-width:200px;background:#fff;border:1px solid #ccc;padding:0.5em 0;box-shadow:0 2px 8px rgba(0,0,0,0.1);">
<button class="dropdown-item" type="button" onclick="copyShareLink('{{ canonical|e('js') }}', this)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="vertical-align:middle;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h6m0 0v6m0-6L10 19l-7-7" />
</svg>
Share Newsroom Link
</button>
{# <button class="dropdown-item" type="button" onclick="copyShareLink('30023:{{ article.pubkey }}:{{ article.slug }}', this)">#}
{# <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="vertical-align:middle;">#}
{# <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12H9m12 0A9 9 0 11 3 12a9 9 0 0118 0z" />#}
{# </svg>#}
{# Share naddr#}
{# </button>#}
</div>
</div>
<script>
// Share dropdown toggle
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('shareBtn');
var menu = document.getElementById('shareDropdown');
btn.addEventListener('click', function(e) {
e.stopPropagation();
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
});
document.addEventListener('click', function() {
menu.style.display = 'none';
});
});
// Copy to clipboard with feedback
function copyShareLink(text, el) {
navigator.clipboard.writeText(text).then(function() {
var orig = el.innerHTML;
el.innerHTML = 'Copied!';
setTimeout(function() { el.innerHTML = orig; }, 1200);
});
}
</script>
{% endif %}
</div> </div>
<article class="w-container">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h1 class="card-title">{{ article.title }}</h1> <h1 class="card-title">{{ article.title }}</h1>

39
templates/pages/editor.html.twig

@ -13,8 +13,9 @@
{% block body %} {% block body %}
<div class="w-container"> <div class="w-container">
<section>
{% if not is_granted('ROLE_USER') %} {% if not is_granted('ROLE_USER') %}
<div class="notice info"> <div class="notice info mb-4">
<p>A Nostr identity is required to post articles.</p> <p>A Nostr identity is required to post articles.</p>
</div> </div>
{% endif %} {% endif %}
@ -83,5 +84,41 @@
{{ form_end(form) }} {{ form_end(form) }}
</div> </div>
</section>
</div> </div>
{% endblock %} {% endblock %}
{% block aside %}
{# Show recent articles and drafts to load for editing #}
{% if is_granted('ROLE_USER') %}
<div class="w-container">
<section>
<h2>Recent Revisions</h2>
<ul class="list-unstyled">
{% for recent in recentArticles %}
<li class="mb-2">
<a href="{{ path('editor-edit-slug', {slug: recent.slug}) }}">
{{ recent.title }} ({{ recent.publishedAt|date('Y-m-d') }})
</a>
</li>
{% else %}
<li>No recent articles found.</li>
{% endfor %}
</ul>
<h2>Drafts</h2>
<ul class="list-unstyled">
{% for draft in drafts %}
<li>
<a href="{{ path('editor-edit-slug', {slug: draft.slug}) }}">
{{ draft.title }} ({{ draft.updatedAt|date('Y-m-d') }})
</a>
</li>
{% else %}
<li>No drafts found.</li>
{% endfor %}
</ul>
</section>
</div>
{% endif %}
{% endblock %}

Loading…
Cancel
Save