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. 18
      src/Controller/ArticleController.php
  5. 2
      src/Entity/Article.php
  6. 24
      src/Twig/Filters.php
  7. 2
      templates/feedback/form.html.twig
  8. 104
      templates/pages/article.html.twig
  9. 163
      templates/pages/editor.html.twig

33
assets/controllers/share_dropdown_controller.js

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

1
config/packages/twig.yaml

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

18
src/Controller/ArticleController.php

@ -160,11 +160,19 @@ class ArticleController extends AbstractController @@ -160,11 +160,19 @@ class ArticleController extends AbstractController
$article = array_shift($articles);
}
if ($article->getPubkey() === null) {
$user = $this->getUser();
$recentArticles = [];
$drafts = [];
$user = $this->getUser();
if (!!$user) {
$key = new Key();
if (!!$user) {
$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);
}
}
@ -176,6 +184,8 @@ class ArticleController extends AbstractController @@ -176,6 +184,8 @@ class ArticleController extends AbstractController
return $this->render('pages/editor.html.twig', [
'article' => $article,
'form' => $form->createView(),
'recentArticles' => $recentArticles,
'drafts' => $drafts,
]);
}

2
src/Entity/Article.php

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

24
src/Twig/Filters.php

@ -4,8 +4,16 @@ declare(strict_types=1); @@ -4,8 +4,16 @@ declare(strict_types=1);
namespace App\Twig;
use App\Entity\Article;
use App\Entity\Event as EventEntity;
use BitWasp\Bech32\Exception\Bech32Exception;
use Exception;
use swentel\nostr\Event\Event;
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\TwigFilter;
@ -18,6 +26,7 @@ class Filters extends AbstractExtension @@ -18,6 +26,7 @@ class Filters extends AbstractExtension
new TwigFilter('linkify', [$this, 'linkify'], ['is_safe' => ['html']]),
new TwigFilter('mentionify', [$this, 'mentionify'], ['is_safe' => ['html']]),
new TwigFilter('nEncode', [$this, 'nEncode']),
new TwigFilter('naddrEncode', [$this, 'naddrEncode']),
];
}
@ -71,4 +80,19 @@ class Filters extends AbstractExtension @@ -71,4 +80,19 @@ class Filters extends AbstractExtension
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 @@ @@ -3,7 +3,7 @@
{% block body %}
<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>
<form data-controller="nostr-single-sign" data-nostr-single-sign-event-value='{{ {
"kind": 24,

104
templates/pages/article.html.twig

@ -16,68 +16,58 @@ @@ -16,68 +16,58 @@
<twig:Organisms:MagazineHero :mag="mag" :magazine="magazine" />
{% endif %}
<div class="article-actions">
{% if canEdit %}
<a class="btn btn-primary" href="{{ path('editor-edit-slug', {'slug': article.slug}) }}">Edit article</a>
{% endif %}
{% if app.user %}
<twig:ReadingListDropdown coordinate="30023:{{ article.pubkey }}:{{ article.slug }}" />
{% endif %}
<article class="w-container">
<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 %}
<a class="btn btn-primary" href="{{ path('editor-edit-slug', {'slug': article.slug}) }}">Edit article</a>
{% endif %}
{% if is_granted('ROLE_ADMIN') %}
<button class="btn btn-primary" onclick="navigator.clipboard.writeText('30023:{{ article.pubkey }}:{{ article.slug }}')">
Copy coordinates
</button>
{% endif %}
{% if app.user %}
<twig:ReadingListDropdown coordinate="30023:{{ article.pubkey }}:{{ article.slug }}" />
{% 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
{% if is_granted('ROLE_ADMIN') %}
<button class="btn btn-primary" onclick="navigator.clipboard.writeText('30023:{{ article.pubkey }}:{{ article.slug }}')">
Copy coordinates
</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>
{% endif %}
</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>
<article class="w-container">
<div class="card">
<div class="card-header">
<h1 class="card-title">{{ article.title }}</h1>

163
templates/pages/editor.html.twig

@ -13,75 +13,112 @@ @@ -13,75 +13,112 @@
{% block body %}
<div class="w-container">
{% if not is_granted('ROLE_USER') %}
<div class="notice info">
<p>A Nostr identity is required to post articles.</p>
</div>
{% endif %}
<div {{ stimulus_controller('nostr-publish', {
publishUrl: path('api-article-publish'),
csrfToken: csrf_token('nostr_publish')
<section>
{% if not is_granted('ROLE_USER') %}
<div class="notice info mb-4">
<p>A Nostr identity is required to post articles.</p>
</div>
{% endif %}
<div {{ stimulus_controller('nostr-publish', {
publishUrl: path('api-article-publish'),
csrfToken: csrf_token('nostr_publish')
}) }} data-nostr-publish-target="form" data-slug="{{ article.slug|default('') }}">
<!-- Status messages -->
<div data-nostr-publish-target="status"></div>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.summary) }}
{{ form_row(form.content) }}
{{ form_row(form.image) }}
<div class="actions" data-controller="image-upload">
<button type="button"
class="btn btn-secondary"
data-action="click->image-upload#openDialog">
Upload Image
</button>
<div data-image-upload-target="dialog" class="iu-dialog">
<div class="iu-backdrop" data-action="click->image-upload#closeDialog"></div>
<div class="iu-modal">
<div class="modal-header">
<h5>Upload Image</h5>
<button type="button" class="close" data-action="click->image-upload#closeDialog">&times;</button>
</div>
<div class="modal-body">
<div>
<label for="upload-provider">Upload to</label>
<select id="upload-provider" data-image-upload-target="provider">
<option value="sovbit">files.sovbit.host</option>
<option value="nostrbuild">nostr.build</option>
<option value="nostrcheck">nostrcheck.me</option>
</select>
</div>
<div data-image-upload-target="dropArea" class="upload-area">
<span>Drag &amp; drop or click to select an image</span>
<input type="file" accept="image/*" data-image-upload-target="fileInput">
</div>
<div data-image-upload-target="progress" class="upload-progress"></div>
<div data-image-upload-target="error" class="upload-error"></div>
</div>
</div>
</div>
</div>
<!-- Status messages -->
<div data-nostr-publish-target="status"></div>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.summary) }}
{{ form_row(form.content) }}
{{ form_row(form.image) }}
<div class="actions" data-controller="image-upload">
<button type="button"
class="btn btn-secondary"
data-action="click->image-upload#openDialog">
Upload Image
</button>
<div data-image-upload-target="dialog" class="iu-dialog">
<div class="iu-backdrop" data-action="click->image-upload#closeDialog"></div>
<div class="iu-modal">
<div class="modal-header">
<h5>Upload Image</h5>
<button type="button" class="close" data-action="click->image-upload#closeDialog">&times;</button>
</div>
<div class="modal-body">
<div>
<label for="upload-provider">Upload to</label>
<select id="upload-provider" data-image-upload-target="provider">
<option value="sovbit">files.sovbit.host</option>
<option value="nostrbuild">nostr.build</option>
<option value="nostrcheck">nostrcheck.me</option>
</select>
</div>
<div data-image-upload-target="dropArea" class="upload-area">
<span>Drag &amp; drop or click to select an image</span>
<input type="file" accept="image/*" data-image-upload-target="fileInput">
</div>
<div data-image-upload-target="progress" class="upload-progress"></div>
<div data-image-upload-target="error" class="upload-error"></div>
</div>
</div>
</div>
</div>
{{ form_row(form.topics) }}
<div class="actions">
<button type="button"
class="btn btn-primary"
data-nostr-publish-target="publishButton"
data-action="click->nostr-publish#publish">
Publish
</button>
{{ form_row(form.topics) }}
<div class="actions">
<button type="button"
class="btn btn-primary"
data-nostr-publish-target="publishButton"
data-action="click->nostr-publish#publish">
Publish
</button>
</div>
{{ form_end(form) }}
</div>
</section>
</div>
{% 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>
{{ form_end(form) }}
<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>
</div>
{% endif %}
{% endblock %}

Loading…
Cancel
Save