24 changed files with 126 additions and 961 deletions
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
tags: |
||||
- ['d', 'newsroom-magazine-by-newsroom-cat-arts'] |
||||
- ['type', 'magazine'] |
||||
- ['l', 'en, ISO-639-1'] |
||||
- ['reading-direction', 'left-to-right, top-to-bottom'] |
||||
- ['t', 'art'] |
||||
- ['title', 'Arts'] |
||||
- ['summary', 'Arts and artistic pursuits'] |
||||
- ['published_by', 'Newsroom magazine'] |
||||
- [ 'p', '7c3bd3d882304e1fb21bbe1d3eb914a6152c747f315fbed6893e99f7119048ef' ] |
||||
- ['a', '30023:e844b39d850acbb13bba1a20057250fe6b3deff5f1ecc95b6a99dc35adafb6a2:awfl8cYr_4oS1-EiY0uP2'] |
||||
- ['a', '30023:5188521b32d40b740a1388d244e22642da3515c0fb39f07057129370008eb518:Legacy-z62rgd'] |
||||
- ['a', '30023:63d59db8d29abe29db7380beb912b8f600237332b6c9978b208694e4be170f6f:Oana-Bakovi-tf09mu'] |
||||
- ['a', '30023:5708b1f601ca8c453ebf622426acc9138cf5b714e9a6ff83c4758c4f208df3b8:Music-neuroplasticity-and-mental-health-f6oavz'] |
||||
- ['a', '30023:63d59db8d29abe29db7380beb912b8f600237332b6c9978b208694e4be170f6f:feature:-katerina-kouzmitcheva'] |
||||
- ['a', '30023:1b30d27ae3a493ad07fe2507700d7b412a7d551e13099774c71347473cf04146:AI-and-the-Future-of-Sound-Transforming-Music-Creation-and-Experience-gi8u8w'] |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
tags: |
||||
- ['d', 'newsroom-magazine-by-newsroom-cat-digital'] |
||||
- ['type', 'magazine'] |
||||
- ['l', 'en, ISO-639-1'] |
||||
- ['reading-direction', 'left-to-right, top-to-bottom'] |
||||
- ['t', 'technology'] |
||||
- ['title', 'Digital'] |
||||
- ['summary', 'Current digital technologies, trends and opportunities'] |
||||
- ['published_by', 'Newsroom magazine'] |
||||
- [ 'p', '7c3bd3d882304e1fb21bbe1d3eb914a6152c747f315fbed6893e99f7119048ef' ] |
||||
- ['a', '30023:dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319:1749882441713'] |
||||
- ['a', '30023:624d01ef570a3730afa1ebedc3ed95d57259ac5f37a9f0eac9c2a0d2f122bf4a:1750083282192'] |
||||
- ['a', '30023:3b7fc823611f1aeaea63ee3bf69b25b8aa16ec6e81d1afc39026808fe194354f:8_keUAm8-jlZIDUhjzJ4L'] |
||||
- ['a', '30023:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1749232910842'] |
||||
- ['a', '30023:7bc05901cbcbbbbb87b76aa42ade4b5873d3d009311975e7fdb7766e8c26d22b:Extra-Extra-Read-all-about-it-4w9ksd'] |
||||
- ['a', '30023:3c389c8f4d46ca81316743a3e33cedb1d0619f8778ee74d47265775e7a2eff7f:1742920054921'] |
||||
- ['a', '30023:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1741197956785'] |
||||
- ['a', '30023:378562cd20849dce3b74d85bc3e72c84f8ab59e94aa29650e1ad1b47a6fc6773:8IphCBoygPWv90xkEHlk0'] |
||||
- ['a', '30023:a012dc823bcf80d5eadf4b6683035634bc40f1b0a52b958278b6bbc96458a70d:2-wTPCMqGqU7H2ptDSelh'] |
||||
- ['a', '30023:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1738257057128'] |
||||
- ['a', '30023:65038d696843e53ff11c3316a5b1c1c71b2fdc31bffe0bfcb93b1e4c1fff8852:2025-01-06-technology-never-changes'] |
||||
- ['a', '30023:4234223996ce6549720e66dd6bc4bb7efb9f25c60c4816d7bc47a65e1d80db24:1724998869566' ] |
||||
- ['a', '30023:65038d696843e53ff11c3316a5b1c1c71b2fdc31bffe0bfcb93b1e4c1fff8852:2024-09-23-delete-the-technology'] |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
tags: |
||||
- ['d', 'newsroom-magazine-by-newsroom-cat-insight'] |
||||
- ['type', 'magazine'] |
||||
- ['l', 'en, ISO-639-1'] |
||||
- ['reading-direction', 'left-to-right, top-to-bottom'] |
||||
- ['t', 'technology'] |
||||
- ['t', 'business'] |
||||
- ['t', 'politics'] |
||||
- ['t', 'economics'] |
||||
- ['title', 'Insight'] |
||||
- ['summary', 'Technology, politics, economics and more'] |
||||
- ['published_by', 'Newsroom magazine'] |
||||
- ['p', '7c3bd3d882304e1fb21bbe1d3eb914a6152c747f315fbed6893e99f7119048ef' ] |
||||
- ['a', '30023:6ad3e2a34818b153c81f48c58f44e5199e7b4fc8dbe37810a000dce3c90b7740:Protocol-ibo4to'] |
||||
- ['a', '30023:4d41a7cbc9b7f7e8484d15499aa9141c73f9a39c3765cc5d5631fa1e7d3633cc:The-dollar-a-broken-contract-7loce5'] |
||||
- ['a', '30023:4234223996ce6549720e66dd6bc4bb7efb9f25c60c4816d7bc47a65e1d80db24:1736500765505' ] |
||||
- ['a', "30023:bc6ccd13a32f94d36564dac8f5963a0d69c583c9968341bbdf278b21f53098e4:money-flows-aren't-important"] |
||||
- ['a', '30023:472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e:japan-admits-it-has-a-problem-and-consumer-debt-hits-a-wall-in-the-us'] |
||||
- ['a', '30023:bc52210b20d3fb89326463a3518674c7edde65794a7765c7f3a9119b20bfc6de:compact-nuclear-power-revolutionizing-energy-with-modular-thorium-reactors'] |
||||
- ['a', '30023:c066aac504e1228a5853117624f2f9156b1f5b332be74bb08f281c136a41a034:purchase-vs-investment'] |
||||
- ['a', '30023:04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9:Capitalism-Scales-Best-Be-Profitable-1rpxxj'] |
||||
- ['a', '30023:ed5774ac84f70a9e1ca14027e07de8e643cef5dcf3e3593266886bb745611c5c:v9WY4L0gdPul3slaO5bt2' ] |
||||
- ['a', '30023:a396e36e962a991dac21731dd45da2ee3fd9265d65f9839c15847294ec991f1c:Bitcoin-as-Escape-from-Clown-Insight'] |
||||
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
tags: |
||||
- ['d', 'newsroom-magazine-by-newsroom-cat-lifestyle'] |
||||
- ['type', 'magazine'] |
||||
- ['l', 'en, ISO-639-1'] |
||||
- ['reading-direction', 'left-to-right, top-to-bottom'] |
||||
- ['t', 'lifestyle'] |
||||
- ['title', 'Lifestyle'] |
||||
- ['summary', 'Lifestyle, health and wellness, travel and home'] |
||||
- ['published_by', 'Newsroom magazine'] |
||||
- [ 'p', '7c3bd3d882304e1fb21bbe1d3eb914a6152c747f315fbed6893e99f7119048ef' ] |
||||
- ['a', '30023:a3c6f928a920617444d8fd10ba65c7a21c75a280cac58ebadd0bb1ded45494fb:24127d1869d6fb0e'] |
||||
- ['a', '30023:04ed2b8f0bbe0a1330f27831108fc75379267826d544f6abb7aac50275be6756:47b47035a8b7bef0'] |
||||
- ['a', '30023:c69b71dc564fdc350acddff929f25d7202ac1470c87488608bd6d98e426ba763:wj6WLkUfYkGRaDK48ZfK8'] |
||||
- ['a', '30023:554ab6fed675985d6b99b79ad7b784ff8ca4a5b02778812e1968eee0c6cbc27e:XTbk9KAgURJbCPniEu_9g'] |
||||
- ['a', '30023:c69b71dc564fdc350acddff929f25d7202ac1470c87488608bd6d98e426ba763:QxHZzjc6kVnE1KBrKhvhc'] |
||||
- ['a', '30023:b8ca3d82ac72d2bdf0d94541898397467d59c9614fa4ce88066cbf8fe28bd6b5:Why-your-body-is-your-way-in-pe04jk'] |
||||
- ['a', '30023:6830c4094f42a138b333157c130a5353b4149cc683e2c18c6e004e22ff17c655:uvU5PQxjIOE4v5dNU3z2b'] |
||||
- ['a', '30023:554ab6fed675985d6b99b79ad7b784ff8ca4a5b02778812e1968eee0c6cbc27e:Health-Benefits-in-Nature-5eptsc'] |
||||
- ['a', '30023:db01672ec0b5219ca521f5954f99ea3560e34fc427f47b9416486c3d9f775028:1743584409108'] |
||||
- ['a', '30023:7d33ba57d8a6e8869a1f1d5215254597594ac0dbfeb01b690def8c461b82db35:DGK40bomu58dGKNVzFioK'] |
||||
- ['a', '30023:6ad3e2a34818b153c81f48c58f44e5199e7b4fc8dbe37810a000dce3c90b7740:igQitdH5ol42zbap8pGSd'] |
||||
- ['a', '30023:378562cd20849dce3b74d85bc3e72c84f8ab59e94aa29650e1ad1b47a6fc6773:Life-Beyond-Bitcoin-2hm81r'] |
||||
- ['a', '30023:378562cd20849dce3b74d85bc3e72c84f8ab59e94aa29650e1ad1b47a6fc6773:The-Benefits-of-Hiking-in-Nature-pcvs1q'] |
||||
- ['a', '30023:0b118e40d6f3dfabb17f21a94a647701f140d8b063a9e84fe6e483644edc09cb:1742976772735'] |
||||
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
tags: |
||||
- ['d', 'newsroom-magazine-by-newsroom-cat-reflections'] |
||||
- ['type', 'magazine'] |
||||
- ['l', 'en, ISO-639-1'] |
||||
- ['reading-direction', 'left-to-right, top-to-bottom'] |
||||
- ['t', 'philosophy'] |
||||
- ['t', 'religion'] |
||||
- ['t', 'spirituality'] |
||||
- ['title', 'Reflections'] |
||||
- ['summary', 'Religion, philosophy and spirituality'] |
||||
- ['published_by', 'Newsroom magazine'] |
||||
- [ 'p', '7c3bd3d882304e1fb21bbe1d3eb914a6152c747f315fbed6893e99f7119048ef' ] |
||||
- ['a', '30023:36f96bcb0bb24a522ce2c2e867cc3548ea4c48f951ae49d24fa30f742ff3888b:sovereign-stoic-why-mastering-yourself-and-your-money-is-the-only-way-forward'] |
||||
- ['a' , '30023:96859d1496ab212d80629b3929fdb8fda7850f7fdd0b31fb265e6844b06bf54d:the-fire-in-the-freezing-forest'] |
||||
- ['a', '30023:16d114303d8203115918ca34a220e925c022c09168175a5ace5e9f3b61640947:8c5cf16c40358825'] |
||||
- ['a', '30023:d57360cb86796680f1af1830d7c33834fe7b57c580e1c598eab89e794fe7d935:Dreams-vs-Reality-806ivo'] |
||||
- ['a', '30023:8d34bd2432240c5637174a3db191878baa1c133aec739b64a264259f414be32b:Miserable-Comforters-Are-You-6kanl1'] |
||||
- ['a', '30023:6ad3e2a34818b153c81f48c58f44e5199e7b4fc8dbe37810a000dce3c90b7740:Science-0iucu9'] |
||||
- ['a', '30023:0c65eba8a81341439ac132a0fec38121ce7d68d89e56a5c2b6e88d784a08ef9a:the-real-goal-of-parenting:-raising-the-parents-of-your-grandchildren-'] |
||||
- ['a', '30023:6e0ea5d6ad5334b4e83354f06bbc1bcd90c09a8576e52bb58b7bfa9b0327f353:1746369859906'] |
||||
- ['a', '30023:8d34bd2432240c5637174a3db191878baa1c133aec739b64a264259f414be32b:Getting-to-Know-God-Through-the-Bible-hxj284'] |
||||
- ['a', '30023:f1f5954911a1c286c04be6409b773703a7c5c246607b51527517c58af4121cfe:Spiritual-Arrogance-co4zoh'] |
||||
- ['a', '30023:554ab6fed675985d6b99b79ad7b784ff8ca4a5b02778812e1968eee0c6cbc27e:0MD-jaH3Nk_VeouWXNWVB'] |
||||
- ['a', '30023:e111a40578d91eff049a350070c17536cc447cff9ec8c4abfe45d810fa441558:1743962847753'] |
||||
- ['a', '30023:378562cd20849dce3b74d85bc3e72c84f8ab59e94aa29650e1ad1b47a6fc6773:Living-a-Godly-Life-in-an-Ungodly-World-pv4d5u'] |
||||
- ['a', '30023:c1e9ab3a56a2ab6ca4bebf44ea64b2fda40ac6311e886ba86b4652169cb56b43:c0d4f9fc46fe7b39'] |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
tags: |
||||
- ['d', 'newsroom-magazine-by-newsroom'] |
||||
- ['type', 'magazine'] |
||||
- ['l', 'en, ISO-639-1'] |
||||
- ['reading-direction', 'left-to-right, top-to-bottom'] |
||||
- ['title', 'Newsroom magazine'] |
||||
- ['summary', 'The first curated magazine of open content'] |
||||
- ['published_by', 'Newsroom magazine'] |
||||
- ['p', '7c3bd3d882304e1fb21bbe1d3eb914a6152c747f315fbed6893e99f7119048ef'] |
||||
- ['a', '30040:7c3bd3d882304e1fb21bbe1d3eb914a6152c747f315fbed6893e99f7119048ef:newsroom-magazine-by-newsroom-cat-insight'] |
||||
- ['a', '30040:7c3bd3d882304e1fb21bbe1d3eb914a6152c747f315fbed6893e99f7119048ef:newsroom-magazine-by-newsroom-cat-digital'] |
||||
- ['a', '30040:7c3bd3d882304e1fb21bbe1d3eb914a6152c747f315fbed6893e99f7119048ef:newsroom-magazine-by-newsroom-cat-lifestyle'] |
||||
- ['a', '30040:7c3bd3d882304e1fb21bbe1d3eb914a6152c747f315fbed6893e99f7119048ef:newsroom-magazine-by-newsroom-cat-reflections'] |
||||
- ['a', '30040:7c3bd3d882304e1fb21bbe1d3eb914a6152c747f315fbed6893e99f7119048ef:newsroom-magazine-by-newsroom-cat-arts'] |
||||
@ -1,69 +0,0 @@
@@ -1,69 +0,0 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace App\Command; |
||||
|
||||
use App\Entity\Article; |
||||
use App\Enum\IndexStatusEnum; |
||||
use Doctrine\ORM\EntityManagerInterface; |
||||
use FOS\ElasticaBundle\Persister\ObjectPersister; |
||||
use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; |
||||
use Symfony\Component\Console\Attribute\AsCommand; |
||||
use Symfony\Component\Console\Command\Command; |
||||
use Symfony\Component\Console\Input\InputInterface; |
||||
use Symfony\Component\Console\Output\OutputInterface; |
||||
|
||||
#[AsCommand(name: 'articles:qa', description: 'Mark articles by quality and select which to index')] |
||||
class QualityCheckArticlesCommand extends Command |
||||
{ |
||||
public function __construct( |
||||
private readonly EntityManagerInterface $entityManager |
||||
) |
||||
{ |
||||
parent::__construct(); |
||||
} |
||||
protected function execute(InputInterface $input, OutputInterface $output): int |
||||
{ |
||||
$articles = $this->entityManager->getRepository(Article::class)->findBy(['indexStatus' => IndexStatusEnum::NOT_INDEXED]); |
||||
$count = 0; |
||||
foreach ($articles as $article) { |
||||
if ($this->meetsCriteria($article)) { |
||||
$count += 1; |
||||
$article->setIndexStatus(IndexStatusEnum::TO_BE_INDEXED); |
||||
} else { |
||||
$article->setIndexStatus(IndexStatusEnum::DO_NOT_INDEX); |
||||
} |
||||
$this->entityManager->persist($article); |
||||
} |
||||
|
||||
$this->entityManager->flush(); |
||||
|
||||
$output->writeln($count . ' articles marked for indexing successfully.'); |
||||
|
||||
return Command::SUCCESS; |
||||
} |
||||
|
||||
private function meetsCriteria(Article $article): bool |
||||
{ |
||||
$content = $article->getContent(); |
||||
|
||||
// No empty title |
||||
if (empty($article->getTitle()) || strtolower($article->getTitle()) === 'test') { |
||||
return false; |
||||
} |
||||
|
||||
// Do not index stacker news reposts |
||||
if (str_contains($content, 'originally posted at https://stacker.news')) { |
||||
return false; |
||||
} |
||||
|
||||
// Slug must not contain '/' and should not be empty |
||||
if (empty($article->getSlug()) || str_contains($article->getSlug(), '/')) { |
||||
return false; |
||||
} |
||||
|
||||
// Only index articles with more than 12 words |
||||
return str_word_count($article->getContent()) > 12; |
||||
} |
||||
} |
||||
@ -1,259 +0,0 @@
@@ -1,259 +0,0 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace App\Controller; |
||||
|
||||
use App\Entity\Article; |
||||
use App\Entity\Event as EventEntity; |
||||
use App\Entity\Nzine; |
||||
use App\Entity\User; |
||||
use App\Enum\KindsEnum; |
||||
use App\Form\NzineBotType; |
||||
use App\Form\NzineType; |
||||
use App\Service\EncryptionService; |
||||
use App\Service\NostrClient; |
||||
use App\Service\NzineWorkflowService; |
||||
use Doctrine\ORM\EntityManagerInterface; |
||||
use Doctrine\Persistence\ManagerRegistry; |
||||
use Exception; |
||||
use swentel\nostr\Event\Event; |
||||
use swentel\nostr\Sign\Sign; |
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
use Symfony\Component\HttpFoundation\Response; |
||||
use Symfony\Component\Routing\Attribute\Route; |
||||
use Symfony\Component\Serializer\Encoder\JsonEncoder; |
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; |
||||
use Symfony\Component\Serializer\Serializer; |
||||
use Symfony\Component\String\Slugger\AsciiSlugger; |
||||
|
||||
class NzineController extends AbstractController |
||||
{ |
||||
/** |
||||
* @throws \JsonException |
||||
*/ |
||||
#[Route('/nzine', name: 'nzine_index')] |
||||
public function index(Request $request, NzineWorkflowService $nzineWorkflowService, EntityManagerInterface $entityManager): Response |
||||
{ |
||||
$form = $this->createForm(NzineBotType::class); |
||||
$form->handleRequest($request); |
||||
$user = $this->getUser(); |
||||
|
||||
$nzine = $entityManager->getRepository(Nzine::class)->findAll(); |
||||
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) { |
||||
$data = $form->getData(); |
||||
// init object |
||||
$nzine = $nzineWorkflowService->init(); |
||||
// create bot and nzine, save to persistence |
||||
$nzine = $nzineWorkflowService->createProfile($nzine, $data['name'], $data['about'], $user); |
||||
// create main index |
||||
$nzineWorkflowService->createMainIndex($nzine, $data['name'], $data['about']); |
||||
|
||||
return $this->redirectToRoute('nzine_edit', ['npub' => $nzine->getNpub() ]); |
||||
} |
||||
|
||||
return $this->render('pages/nzine-editor.html.twig', [ |
||||
'form' => $form |
||||
]); |
||||
} |
||||
|
||||
#[Route('/nzine/{npub}', name: 'nzine_edit')] |
||||
public function edit(Request $request, $npub, EntityManagerInterface $entityManager, |
||||
EncryptionService $encryptionService, |
||||
ManagerRegistry $managerRegistry, NostrClient $nostrClient): Response |
||||
{ |
||||
$nzine = $entityManager->getRepository(Nzine::class)->findOneBy(['npub' => $npub]); |
||||
if (!$nzine) { |
||||
throw $this->createNotFoundException('N-Zine not found'); |
||||
} |
||||
try { |
||||
$bot = $entityManager->getRepository(User::class)->findOneBy(['npub' => $npub]); |
||||
} catch (Exception $e) { |
||||
// sth went wrong, but whatever |
||||
$managerRegistry->resetManager(); |
||||
} |
||||
|
||||
// existing index |
||||
$indices = $entityManager->getRepository(EventEntity::class)->findBy(['pubkey' => $npub, 'kind' => KindsEnum::PUBLICATION_INDEX]); |
||||
|
||||
$mainIndexCandidates = array_filter($indices, function ($index) use ($nzine) { |
||||
return $index->getSlug() == $nzine->getSlug(); |
||||
}); |
||||
|
||||
$mainIndex = array_pop($mainIndexCandidates); |
||||
if (empty($mainIndex)) { |
||||
throw $this->createNotFoundException('Main index not found'); |
||||
} |
||||
|
||||
$catForm = $this->createForm(NzineType::class, ['categories' => $nzine->getMainCategories()]); |
||||
$catForm->handleRequest($request); |
||||
|
||||
if ($catForm->isSubmitted() && $catForm->isValid()) { |
||||
// Process and normalize the 'tags' field |
||||
$data = $catForm->get('categories')->getData(); |
||||
|
||||
$nzine->setMainCategories($data); |
||||
|
||||
try { |
||||
$entityManager->beginTransaction(); |
||||
$entityManager->persist($nzine); |
||||
$entityManager->flush(); |
||||
$entityManager->commit(); |
||||
} catch (Exception $e) { |
||||
$entityManager->rollback(); |
||||
$managerRegistry->resetManager(); |
||||
} |
||||
|
||||
$catIndices = []; |
||||
|
||||
$bot = $nzine->getNzineBot(); |
||||
$bot->setEncryptionService($encryptionService); |
||||
$private_key = $bot->getNsec(); // decrypted en route |
||||
|
||||
foreach ($data as $cat) { |
||||
// check if such an index exists, only create new cats |
||||
$id = array_filter($indices, function ($k) use ($cat) { |
||||
return $cat['title'] === $k->getTitle(); |
||||
}); |
||||
if (!empty($id)) { continue; } |
||||
|
||||
// create new index |
||||
// currently not possible to edit existing, because there is no way to tell what has changed |
||||
// and which is the corresponding event |
||||
$slugger = new AsciiSlugger(); |
||||
$title = $cat['title']; |
||||
$slug = $mainIndex->getSlug().'-'.$slugger->slug($title)->lower(); |
||||
// create category index |
||||
$index = new Event(); |
||||
$index->setKind(KindsEnum::PUBLICATION_INDEX->value); |
||||
|
||||
$index->addTag(['d' => $slug]); |
||||
$index->addTag(['title' => $title]); |
||||
$index->addTag(['auto-update' => 'yes']); |
||||
$index->addTag(['type' => 'magazine']); |
||||
foreach ($cat['tags'] as $tag) { |
||||
$index->addTag(['t' => $tag]); |
||||
} |
||||
$index->setPublicKey($nzine->getNpub()); |
||||
|
||||
$signer = new Sign(); |
||||
$signer->signEvent($index, $private_key); |
||||
// save to persistence, first map to EventEntity |
||||
$serializer = new Serializer([new ObjectNormalizer()],[new JsonEncoder()]); |
||||
$i = $serializer->deserialize($index->toJson(), EventEntity::class, 'json'); |
||||
// don't save any more for now |
||||
$entityManager->persist($i); |
||||
$entityManager->flush(); |
||||
// TODO publish index to relays |
||||
|
||||
$catIndices[] = $index; |
||||
} |
||||
|
||||
// add the new and updated indices to the main index |
||||
foreach ($catIndices as $idx) { |
||||
//remove e tags and add new |
||||
// $tags = array_splice($mainIndex->getTags(), -3); |
||||
// $mainIndex->setTags($tags); |
||||
// TODO add relay hints |
||||
$mainIndex->addTag(['a' => KindsEnum::PUBLICATION_INDEX->value .':'. $idx->getPublicKey() .':'. $idx->getSlug()]); |
||||
// $mainIndex->addTag(['e' => $idx->getId() ]); |
||||
} |
||||
|
||||
// re-sign main index and save to relays |
||||
// $signer = new Sign(); |
||||
// $signer->signEvent($mainIndex, $private_key); |
||||
// for now, just save new index |
||||
$entityManager->flush(); |
||||
|
||||
// redirect to route nzine_view |
||||
return $this->redirectToRoute('nzine_view', [ |
||||
'npub' => $nzine->getNpub(), |
||||
]); |
||||
} |
||||
|
||||
return $this->render('pages/nzine-editor.html.twig', [ |
||||
'nzine' => $nzine, |
||||
'indices' => $indices, |
||||
'bot' => $bot ?? null, // if null, the profile for the bot doesn't exist yet |
||||
'catForm' => $catForm |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* Update and (re)publish indices, |
||||
* when you want to look for new articles or |
||||
* when categories have changed |
||||
* @return void |
||||
*/ |
||||
#[Route('/nzine/{npub}', name: 'nzine_update')] |
||||
public function nzineUpdate() |
||||
{ |
||||
// TODO make this a separate step and publish all the indices and populate with articles all at once |
||||
|
||||
} |
||||
|
||||
|
||||
#[Route('/nzine/v/{npub}', name: 'nzine_view')] |
||||
public function nzineView($npub, EntityManagerInterface $entityManager): Response |
||||
{ |
||||
$nzine = $entityManager->getRepository(Nzine::class)->findOneBy(['npub' => $npub]); |
||||
if (!$nzine) { |
||||
throw $this->createNotFoundException('N-Zine not found'); |
||||
} |
||||
// Find all index events for this nzine |
||||
$indices = $entityManager->getRepository(EventEntity::class)->findBy(['pubkey' => $npub, 'kind' => KindsEnum::PUBLICATION_INDEX]); |
||||
$mainIndexCandidates = array_filter($indices, function ($index) use ($nzine) { |
||||
return $index->getSlug() == $nzine->getSlug(); |
||||
}); |
||||
|
||||
dump($indices);die(); |
||||
|
||||
$mainIndex = array_pop($mainIndexCandidates); |
||||
|
||||
return $this->render('pages/nzine.html.twig', [ |
||||
'nzine' => $nzine, |
||||
'index' => $mainIndex, |
||||
'events' => $indices, // TODO traverse all and collect all leaves |
||||
]); |
||||
} |
||||
|
||||
#[Route('/nzine/v/{npub}/{cat}', name: 'nzine_category')] |
||||
public function nzineCategory($npub, $cat, EntityManagerInterface $entityManager): Response |
||||
{ |
||||
$nzine = $entityManager->getRepository(Nzine::class)->findOneBy(['npub' => $npub]); |
||||
if (!$nzine) { |
||||
throw $this->createNotFoundException('N-Zine not found'); |
||||
} |
||||
$bot = $entityManager->getRepository(User::class)->findOneBy(['npub' => $npub]); |
||||
|
||||
$tags = []; |
||||
foreach ($nzine->getMainCategories() as $category) { |
||||
if (isset($category['title']) && $category['title'] === $cat) { |
||||
$tags = $category['tags'] ?? []; |
||||
} |
||||
} |
||||
|
||||
$all = $entityManager->getRepository(Article::class)->findAll(); |
||||
$list = array_slice($all, 0, 100); |
||||
|
||||
$filtered = []; |
||||
foreach ($tags as $tag) { |
||||
$partial = array_filter($list, function($v) use ($tag) { |
||||
/* @var Article $v */ |
||||
return in_array($tag, $v->getTopics() ?? []); |
||||
}); |
||||
$filtered = array_merge($filtered, $partial); |
||||
} |
||||
|
||||
|
||||
return $this->render('pages/nzine.html.twig', [ |
||||
'nzine' => $nzine, |
||||
'bot' => $bot, |
||||
'list' => $filtered |
||||
]); |
||||
} |
||||
|
||||
} |
||||
@ -1,141 +0,0 @@
@@ -1,141 +0,0 @@
|
||||
<?php |
||||
|
||||
namespace App\Entity; |
||||
|
||||
use App\Repository\NzineRepository; |
||||
use Doctrine\Common\Collections\ArrayCollection; |
||||
use Doctrine\DBAL\Types\Types; |
||||
use Doctrine\ORM\Mapping as ORM; |
||||
|
||||
#[ORM\Entity(repositoryClass: NzineRepository::class)] |
||||
class Nzine |
||||
{ |
||||
#[ORM\Id] |
||||
#[ORM\GeneratedValue] |
||||
#[ORM\Column] |
||||
private ?int $id = null; |
||||
|
||||
#[ORM\Column(length: 255)] |
||||
private ?string $npub = null; |
||||
|
||||
#[ORM\OneToOne(targetEntity: NzineBot::class)] |
||||
#[ORM\JoinColumn(nullable: true)] |
||||
private ?NzineBot $nzineBot = null; |
||||
|
||||
#[ORM\Column(type: Types::JSON)] |
||||
private array|ArrayCollection $mainCategories; |
||||
|
||||
#[ORM\Column(nullable: true)] |
||||
private ?array $lists = null; |
||||
|
||||
#[ORM\Column(length: 255, nullable: true)] |
||||
private ?string $editor = null; |
||||
|
||||
/** |
||||
* Slug (d-tag) of the main index event that contains all the main category indices |
||||
* @var string|null |
||||
*/ |
||||
#[ORM\Column(type: Types::TEXT, nullable: true)] |
||||
private ?string $slug = null; |
||||
|
||||
#[ORM\Column(type: Types::STRING)] |
||||
private string $state = 'draft'; |
||||
|
||||
public function __construct() |
||||
{ |
||||
$this->mainCategories = new ArrayCollection(); |
||||
} |
||||
|
||||
public function getId(): ?int |
||||
{ |
||||
return $this->id; |
||||
} |
||||
|
||||
public function getNpub(): ?string |
||||
{ |
||||
return $this->npub; |
||||
} |
||||
|
||||
public function setNpub(string $npub): static |
||||
{ |
||||
$this->npub = $npub; |
||||
|
||||
return $this; |
||||
} |
||||
|
||||
public function getNsec(): ?string |
||||
{ |
||||
return $this->nsec; |
||||
} |
||||
|
||||
public function setNsec(?string $nsec): void |
||||
{ |
||||
$this->nsec = $nsec; |
||||
} |
||||
|
||||
public function getMainCategories(): array |
||||
{ |
||||
return $this->mainCategories; |
||||
} |
||||
|
||||
public function setMainCategories(array $mainCategories): static |
||||
{ |
||||
$this->mainCategories = $mainCategories; |
||||
|
||||
return $this; |
||||
} |
||||
|
||||
public function getLists(): ?array |
||||
{ |
||||
return $this->lists; |
||||
} |
||||
|
||||
public function setLists(?array $lists): static |
||||
{ |
||||
$this->lists = $lists; |
||||
|
||||
return $this; |
||||
} |
||||
|
||||
public function getEditor(): ?string |
||||
{ |
||||
return $this->editor; |
||||
} |
||||
|
||||
public function setEditor(?string $editor): static |
||||
{ |
||||
$this->editor = $editor; |
||||
|
||||
return $this; |
||||
} |
||||
|
||||
public function getNzineBot(): ?NzineBot |
||||
{ |
||||
return $this->nzineBot; |
||||
} |
||||
|
||||
public function setNzineBot(?NzineBot $nzineBot): void |
||||
{ |
||||
$this->nzineBot = $nzineBot; |
||||
} |
||||
|
||||
public function getSlug(): ?string |
||||
{ |
||||
return $this->slug; |
||||
} |
||||
|
||||
public function setSlug(?string $slug): void |
||||
{ |
||||
$this->slug = $slug; |
||||
} |
||||
|
||||
public function getState(): string |
||||
{ |
||||
return $this->state; |
||||
} |
||||
|
||||
public function setState(string $state): void |
||||
{ |
||||
$this->state = $state; |
||||
} |
||||
} |
||||
@ -1,51 +0,0 @@
@@ -1,51 +0,0 @@
|
||||
<?php |
||||
|
||||
namespace App\Entity; |
||||
|
||||
use App\Service\EncryptionService; |
||||
use Doctrine\DBAL\Types\Types; |
||||
use Doctrine\ORM\Mapping as ORM; |
||||
use Symfony\Component\Serializer\Annotation\Ignore; |
||||
|
||||
|
||||
#[ORM\Entity] |
||||
class NzineBot |
||||
{ |
||||
#[ORM\Id] |
||||
#[ORM\GeneratedValue] |
||||
#[ORM\Column(type: Types::INTEGER)] |
||||
private ?int $id = null; |
||||
|
||||
private ?EncryptionService $encryptionService = null; |
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255)] |
||||
private ?string $encryptedNsec = null; |
||||
|
||||
#[Ignore] |
||||
private ?string $nsec = null; |
||||
|
||||
public function setEncryptionService(EncryptionService $encryptionService): void |
||||
{ |
||||
$this->encryptionService = $encryptionService; |
||||
} |
||||
|
||||
public function getId(): ?int |
||||
{ |
||||
return $this->id; |
||||
} |
||||
|
||||
public function getNsec(): ?string |
||||
{ |
||||
if (null === $this->nsec && null !== $this->encryptedNsec) { |
||||
$this->nsec = $this->encryptionService->decrypt($this->encryptedNsec); |
||||
} |
||||
return $this->nsec; |
||||
} |
||||
|
||||
public function setNsec(?string $nsec): self |
||||
{ |
||||
$this->nsec = $nsec; |
||||
$this->encryptedNsec = $this->encryptionService->encrypt($nsec); |
||||
return $this; |
||||
} |
||||
} |
||||
@ -1,11 +0,0 @@
@@ -1,11 +0,0 @@
|
||||
<?php |
||||
|
||||
namespace App\Enum; |
||||
|
||||
enum IndexStatusEnum: int |
||||
{ |
||||
case NOT_INDEXED = 0; |
||||
case TO_BE_INDEXED = 1; |
||||
case INDEXED = 2; |
||||
case DO_NOT_INDEX = 3; |
||||
} |
||||
@ -1,32 +0,0 @@
@@ -1,32 +0,0 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace App\Form; |
||||
|
||||
use Symfony\Component\Form\AbstractType; |
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType; |
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType; |
||||
use Symfony\Component\Form\Extension\Core\Type\TextType; |
||||
use Symfony\Component\Form\FormBuilderInterface; |
||||
use Symfony\Component\OptionsResolver\OptionsResolver; |
||||
|
||||
class NzineBotType extends AbstractType |
||||
{ |
||||
public function buildForm(FormBuilderInterface $builder, array $options): void |
||||
{ |
||||
$builder |
||||
->add('name', TextType::class, [ |
||||
'required' => true |
||||
]) |
||||
->add('about', TextareaType::class, [ |
||||
'required' => false |
||||
]) |
||||
->add('submit', SubmitType::class) |
||||
; |
||||
} |
||||
|
||||
public function configureOptions(OptionsResolver $resolver) |
||||
{ |
||||
} |
||||
} |
||||
@ -1,30 +0,0 @@
@@ -1,30 +0,0 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace App\Form; |
||||
|
||||
use Symfony\Component\Form\AbstractType; |
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType; |
||||
use Symfony\Component\Form\FormBuilderInterface; |
||||
use Symfony\Component\OptionsResolver\OptionsResolver; |
||||
|
||||
class NzineType extends AbstractType |
||||
{ |
||||
public function buildForm(FormBuilderInterface $builder, array $options): void |
||||
{ |
||||
$builder |
||||
->add('categories', CollectionType::class, [ |
||||
'entry_type' => MainCategoryType::class, |
||||
'allow_add' => true, |
||||
'allow_delete' => true, |
||||
'by_reference' => false, |
||||
'prototype' => true, // Enables the JavaScript prototype feature |
||||
'label' => false, |
||||
]); |
||||
} |
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void |
||||
{ |
||||
} |
||||
} |
||||
@ -1,43 +0,0 @@
@@ -1,43 +0,0 @@
|
||||
<?php |
||||
|
||||
namespace App\Repository; |
||||
|
||||
use App\Entity\Nzine; |
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; |
||||
use Doctrine\Persistence\ManagerRegistry; |
||||
|
||||
/** |
||||
* @extends ServiceEntityRepository<Nzine> |
||||
*/ |
||||
class NzineRepository extends ServiceEntityRepository |
||||
{ |
||||
public function __construct(ManagerRegistry $registry) |
||||
{ |
||||
parent::__construct($registry, Nzine::class); |
||||
} |
||||
|
||||
// /** |
||||
// * @return Nzine[] Returns an array of Nzine objects |
||||
// */ |
||||
// public function findByExampleField($value): array |
||||
// { |
||||
// return $this->createQueryBuilder('j') |
||||
// ->andWhere('j.exampleField = :val') |
||||
// ->setParameter('val', $value) |
||||
// ->orderBy('j.id', 'ASC') |
||||
// ->setMaxResults(10) |
||||
// ->getQuery() |
||||
// ->getResult() |
||||
// ; |
||||
// } |
||||
|
||||
// public function findOneBySomeField($value): ?Nzine |
||||
// { |
||||
// return $this->createQueryBuilder('j') |
||||
// ->andWhere('j.exampleField = :val') |
||||
// ->setParameter('val', $value) |
||||
// ->getQuery() |
||||
// ->getOneOrNullResult() |
||||
// ; |
||||
// } |
||||
} |
||||
@ -1,147 +0,0 @@
@@ -1,147 +0,0 @@
|
||||
<?php |
||||
|
||||
namespace App\Service; |
||||
|
||||
use App\Entity\Nzine; |
||||
use App\Entity\NzineBot; |
||||
use App\Entity\Event as EventEntity; |
||||
use App\Entity\User; |
||||
use App\Enum\KindsEnum; |
||||
use App\Enum\RolesEnum; |
||||
use swentel\nostr\Event\Event; |
||||
use swentel\nostr\Key\Key; |
||||
use swentel\nostr\Sign\Sign; |
||||
use Symfony\Component\Serializer\Encoder\JsonEncoder; |
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; |
||||
use Symfony\Component\Serializer\Serializer; |
||||
use Symfony\Component\String\Slugger\AsciiSlugger; |
||||
use Symfony\Component\Workflow\WorkflowInterface; |
||||
use Doctrine\ORM\EntityManagerInterface; |
||||
|
||||
class NzineWorkflowService |
||||
{ |
||||
private Nzine $nzine; |
||||
|
||||
public function __construct(private readonly WorkflowInterface $nzineWorkflow, |
||||
private readonly NostrClient $nostrClient, |
||||
private readonly EncryptionService $encryptionService, |
||||
private readonly EntityManagerInterface $entityManager) |
||||
{ |
||||
} |
||||
|
||||
public function init($nzine = null): Nzine |
||||
{ |
||||
if (!is_null($nzine)) { |
||||
$this->nzine = $nzine; |
||||
} else { |
||||
$this->nzine = new Nzine(); |
||||
} |
||||
|
||||
return $this->nzine; |
||||
} |
||||
|
||||
public function createProfile($nzine, $name, $about, $user): Nzine |
||||
{ |
||||
if (!$this->nzineWorkflow->can($nzine, 'create_profile')) { |
||||
throw new \LogicException('Cannot create profile in the current state.'); |
||||
} |
||||
|
||||
$this->nzine = $nzine; |
||||
|
||||
// create NZine bot |
||||
$key = new Key(); |
||||
$private_key = $key->generatePrivateKey(); |
||||
$bot = new NzineBot(); |
||||
$bot->setEncryptionService($this->encryptionService); |
||||
$bot->setNsec($private_key); |
||||
$this->entityManager->persist($bot); |
||||
$this->entityManager->flush(); |
||||
|
||||
// publish bot profile |
||||
$profileContent = [ |
||||
'name' => $name, |
||||
'about' => $about, |
||||
'bot' => true |
||||
]; |
||||
$profileEvent = new Event(); |
||||
$profileEvent->setKind(KindsEnum::METADATA->value); |
||||
$profileEvent->setContent(json_encode($profileContent)); |
||||
$signer = new Sign(); |
||||
$signer->signEvent($profileEvent, $private_key); |
||||
// $this->nostrClient->publishEvent($profileEvent, ['wss://purplepag.es']); |
||||
|
||||
// add EDITOR role to the user |
||||
$role = RolesEnum::EDITOR->value; |
||||
$user = $this->entityManager->getRepository(User::class)->findOneBy(['npub' => $user->getUserIdentifier()]); |
||||
$user->addRole($role); |
||||
$this->entityManager->persist($user); |
||||
|
||||
// create NZine entity |
||||
$public_key = $key->getPublicKey($private_key); |
||||
$this->nzine->setNpub($public_key); |
||||
$this->nzine->setNzineBot($bot); |
||||
$this->nzine->setEditor($user->getUserIdentifier()); |
||||
$this->nzineWorkflow->apply($this->nzine, 'create_profile'); |
||||
$this->entityManager->persist($this->nzine); |
||||
$this->entityManager->flush(); |
||||
|
||||
return $this->nzine; |
||||
} |
||||
|
||||
/** |
||||
* @throws \JsonException |
||||
*/ |
||||
public function createMainIndex(Nzine $nzine, string $title, string $summary): void |
||||
{ |
||||
if (!$this->nzineWorkflow->can($nzine, 'create_main_index')) { |
||||
// throw new \LogicException('Cannot create main index in the current state.'); |
||||
} |
||||
|
||||
$bot = $nzine->getNzineBot(); |
||||
$private_key = $bot->getNsec(); |
||||
|
||||
$slugger = new AsciiSlugger(); |
||||
$slug = 'nzine-'.$slugger->slug($title)->lower().'-'.rand(10000,99999); |
||||
// save slug to nzine |
||||
$nzine->setSlug($slug); |
||||
|
||||
// create NZine main index |
||||
$index = new Event(); |
||||
$index->setKind(KindsEnum::PUBLICATION_INDEX->value); |
||||
|
||||
$index->addTag(['d' => $slug]); |
||||
$index->addTag(['title' => $title]); |
||||
$index->addTag(['summary' => $summary]); |
||||
$index->addTag(['auto-update' => 'yes']); |
||||
$index->addTag(['type' => 'magazine']); |
||||
$signer = new Sign(); |
||||
$signer->signEvent($index, $private_key); |
||||
// save to persistence, first map to EventEntity |
||||
$serializer = new Serializer([new ObjectNormalizer()],[new JsonEncoder()]); |
||||
$i = $serializer->deserialize($index->toJson(), EventEntity::class, 'json'); |
||||
$this->entityManager->persist($i); |
||||
|
||||
$this->nzineWorkflow->apply($nzine, 'create_main_index'); |
||||
$this->entityManager->flush(); |
||||
} |
||||
|
||||
public function createNestedIndex(Nzine $nzine, string $categoryTitle, array $tags): void |
||||
{ |
||||
if (!$this->nzineWorkflow->can($nzine, 'create_nested_indices')) { |
||||
throw new \LogicException('Cannot create nested indices in the current state.'); |
||||
} |
||||
|
||||
// Example logic: Create a nested index for the category |
||||
$nestedIndex = new EventEntity(); |
||||
// $nestedIndex->setTitle($categoryTitle); |
||||
$nestedIndex->setTags($tags); |
||||
$nestedIndex->setKind('30040'); // Assuming 30040 is the kind for publication indices |
||||
|
||||
$this->entityManager->persist($nestedIndex); |
||||
|
||||
$this->nzineWorkflow->apply($nzine, 'create_nested_indices'); |
||||
$this->entityManager->persist($nzine); |
||||
$this->entityManager->flush(); |
||||
} |
||||
} |
||||
|
||||
@ -1,37 +0,0 @@
@@ -1,37 +0,0 @@
|
||||
<?php |
||||
|
||||
namespace App\Twig\Components\Organisms; |
||||
|
||||
use App\Entity\Event; |
||||
use App\Entity\Nzine; |
||||
use App\Enum\KindsEnum; |
||||
use Doctrine\ORM\EntityManagerInterface; |
||||
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; |
||||
|
||||
#[AsTwigComponent] |
||||
final class ZineList |
||||
{ |
||||
public array $nzines = []; |
||||
public array $indices = []; |
||||
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager) |
||||
{ |
||||
} |
||||
|
||||
public function mount(?array $nzines = null): void |
||||
{ |
||||
$this->nzines = $nzines ?? $this->entityManager->getRepository(Nzine::class)->findAll(); |
||||
if (count($this->nzines) > 0) { |
||||
// find indices for each nzine |
||||
foreach ($this->nzines as $zine) { |
||||
$ids = $this->entityManager->getRepository(Event::class)->findBy(['pubkey' => $zine->getNpub(), 'kind' => KindsEnum::PUBLICATION_INDEX]); |
||||
$id = array_filter($ids, function($k) use ($zine) { |
||||
return $k->getSlug() == $zine->getSlug(); |
||||
}); |
||||
if ($id) { |
||||
$this->indices[$zine->getNpub()] = array_pop($id); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -1,16 +0,0 @@
@@ -1,16 +0,0 @@
|
||||
<div {{ attributes }}> |
||||
{% for item in nzines %} |
||||
{% if item.npub in indices|keys %} |
||||
{% set idx = indices[item.npub] %} |
||||
{% if idx|length > 0 %} |
||||
<a class="card" href="{{ path('nzine_view', { npub: item.npub })}}"> |
||||
<div class="card-body"> |
||||
<h3 class="card-title">{{ idx.title }}</h3> |
||||
<p class="hidden">{{ idx.summary }}</p> |
||||
</div> |
||||
</a> |
||||
<br> |
||||
{% endif %} |
||||
{% endif %} |
||||
{% endfor %} |
||||
</div> |
||||
@ -1,66 +0,0 @@
@@ -1,66 +0,0 @@
|
||||
{% extends 'base.html.twig' %} |
||||
|
||||
{% block body %} |
||||
{% if nzine is not defined %} |
||||
<h1>{{ 'heading.createNzine'|trans }}</h1> |
||||
<twig:Atoms:Alert >N-Zines are in active development. Expect weirdness.</twig:Atoms:Alert> |
||||
<p class="lede"> |
||||
An N-Zine is a digital magazine definition for |
||||
collecting long form articles from the <em>nostr</em> ecosystem according to specified filters. |
||||
The N-Zine can then be read and browsed as a traditional digital magazine made available on this platform. |
||||
Additionally, it can be subscribed to using the <em>nostr</em> bot which will be generated during the setup process. |
||||
Your currently logged-in <em>npub</em> will be assigned to the N-Zine as an editor, so you can come back later and tweak the filters. |
||||
</p> |
||||
|
||||
<h2>N-Zine Details</h2> |
||||
<p> |
||||
Choose a title and write a description for your N-Zine. |
||||
A profile for your N-Zine bot will also be created. |
||||
The bot will publish an update when a new article is found that matches N-Zine's filters. |
||||
<br> |
||||
<small>We know it's lame, but right now we cannot automatically update your follows to include the N-Zine bot.</small> |
||||
</p> |
||||
|
||||
{{ form_start(form) }} |
||||
{{ form_end(form) }} |
||||
|
||||
|
||||
{% else %} |
||||
<h1>{{ 'heading.editNzine'|trans }}</h1> |
||||
|
||||
|
||||
<h2>Indices</h2> |
||||
<ul> |
||||
{% for idx in indices %} |
||||
<li>{{ idx.title }}</li> |
||||
{% endfor %} |
||||
</ul> |
||||
|
||||
|
||||
<h2>Categories</h2> |
||||
<p> |
||||
Create and edit categories. You can have as many as you like. Aim at up to 9 for the sake of your readers. |
||||
</p> |
||||
|
||||
{{ form_start(catForm) }} |
||||
|
||||
<ul class="tags"> |
||||
{% for cat in catForm.categories %} |
||||
<li>{{ form_widget(cat) }}</li> |
||||
{% endfor %} |
||||
</ul> |
||||
|
||||
<div {{ stimulus_controller('form-collection') }} |
||||
data-form-collection-index-value="{{ catForm.categories|length > 0 ? catForm.categories|last.vars.name + 1 : 0 }}" |
||||
data-form-collection-prototype-value="{{ form_widget(catForm.categories.vars.prototype)|e('html_attr') }}" |
||||
> |
||||
<ul {{ stimulus_target('form-collection', 'collectionContainer') }}></ul> |
||||
<button type="button" {{ stimulus_action('form-collection', 'addCollectionElement') }}>Add item</button> |
||||
</div> |
||||
|
||||
|
||||
<button class="btn btn-primary">Save</button> |
||||
{{ form_end(catForm) }} |
||||
|
||||
{% endif %} |
||||
{% endblock %} |
||||
@ -1,16 +0,0 @@
@@ -1,16 +0,0 @@
|
||||
{% extends 'base.html.twig' %} |
||||
|
||||
{% block body %} |
||||
<div> |
||||
<h1>{{ index.title }}</h1> |
||||
<p>{{ index.summary }}</p> |
||||
<br> |
||||
|
||||
<twig:IndexTabs :index="index" /> |
||||
|
||||
</div> |
||||
{% endblock %} |
||||
|
||||
{% block aside %} |
||||
{# <p>TODO search & add to index</p> #} |
||||
{% endblock %} |
||||
Loading…
Reference in new issue