Browse Source

Revise routing, part 1

imwald
Nuša Pukšič 4 months ago
parent
commit
e298269920
  1. 1
      assets/app.js
  2. 115
      assets/controllers/sidebar_toggle_controller.js
  3. 27
      assets/styles/app.css
  4. 6
      assets/styles/button.css
  5. 43
      assets/styles/form.css
  6. 72
      assets/styles/landing.css
  7. 162
      assets/styles/layout.css
  8. 13
      assets/styles/theme.css
  9. 6
      assets/styles/utilities.css
  10. 3
      config/packages/twig.yaml
  11. 1
      config/packages/web_profiler.yaml
  12. 9
      src/Controller/Administration/MagazineAdminController.php
  13. 72
      src/Controller/DefaultController.php
  14. 18
      src/Controller/StaticController.php
  15. 4
      src/Entity/Event.php
  16. 18
      src/Twig/Components/Molecules/CategoryLink.php
  17. 38
      src/Twig/Components/Organisms/ZineList.php
  18. 2
      templates/admin/analytics.html.twig
  19. 2
      templates/admin/articles.html.twig
  20. 2
      templates/admin/magazine_editor.html.twig
  21. 2
      templates/admin/magazines.html.twig
  22. 2
      templates/admin/roles.html.twig
  23. 2
      templates/admin/transactions.html.twig
  24. 18
      templates/base.html.twig
  25. 2
      templates/bundles/TwigBundle/Exception/error.html.twig
  26. 2
      templates/bundles/TwigBundle/Exception/error404.html.twig
  27. 4
      templates/components/Header.html.twig
  28. 9
      templates/components/Molecules/CategoryLink.html.twig
  29. 2
      templates/components/Organisms/ReadingListList.html.twig
  30. 35
      templates/components/Organisms/ZineList.html.twig
  31. 7
      templates/components/UserMenu.html.twig
  32. 2
      templates/event/index.html.twig
  33. 111
      templates/home.html.twig
  34. 5
      templates/layout-full.html.twig
  35. 41
      templates/layout.html.twig
  36. 14
      templates/magazine/magazine_articles.html.twig
  37. 2
      templates/magazine/magazine_review.html.twig
  38. 4
      templates/magazine/magazine_setup.html.twig
  39. 2
      templates/pages/article.html.twig
  40. 2
      templates/pages/author.html.twig
  41. 2
      templates/pages/category.html.twig
  42. 8
      templates/pages/editor.html.twig
  43. 0
      templates/pages/journals.html.twig
  44. 5
      templates/pages/latest.html.twig
  45. 14
      templates/pages/lists.html.twig
  46. 2
      templates/pages/magazine.html.twig
  47. 18
      templates/pages/newsstand.html.twig
  48. 2
      templates/pages/nzine-editor.html.twig
  49. 2
      templates/pages/nzine.html.twig
  50. 8
      templates/pages/search.html.twig
  51. 2
      templates/reading_list/compose.html.twig
  52. 18
      templates/reading_list/index.html.twig
  53. 2
      templates/reading_list/reading_articles.html.twig
  54. 2
      templates/reading_list/reading_review.html.twig
  55. 2
      templates/reading_list/reading_setup.html.twig
  56. 47
      templates/static/about.html.twig
  57. 103
      templates/static/landing.html.twig
  58. 2
      templates/static/pricing.html.twig
  59. 2
      templates/static/roadmap.html.twig
  60. 2
      templates/static/tos.html.twig
  61. 45
      templates/static/unfold.html.twig

1
assets/app.js

@ -20,6 +20,7 @@ import './styles/a2hs.css'; @@ -20,6 +20,7 @@ import './styles/a2hs.css';
import './styles/analytics.css';
import './styles/modal.css';
import './styles/utilities.css';
import './styles/landing.css';
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');

115
assets/controllers/sidebar_toggle_controller.js

@ -0,0 +1,115 @@ @@ -0,0 +1,115 @@
import { Controller } from '@hotwired/stimulus';
/*
* Sidebar toggle controller
* Controls showing/hiding nav (#leftNav) and aside (#rightNav) on mobile viewports.
* Uses aria-controls attribute of clicked toggle buttons.
*/
export default class extends Controller {
static targets = [ ];
connect() {
this.mediaQuery = window.matchMedia('(min-width: 769px)');
this.resizeListener = () => this.handleResize();
this.keyListener = (e) => this.handleKeydown(e);
this.clickOutsideListener = (e) => this.handleDocumentClick(e);
this.mediaQuery.addEventListener('change', this.resizeListener);
document.addEventListener('keydown', this.keyListener);
document.addEventListener('click', this.clickOutsideListener);
this.handleResize();
}
disconnect() {
this.mediaQuery.removeEventListener('change', this.resizeListener);
document.removeEventListener('keydown', this.keyListener);
document.removeEventListener('click', this.clickOutsideListener);
}
toggle(event) {
const controlId = event.currentTarget.getAttribute('aria-controls');
const el = document.getElementById(controlId);
if (!el) return;
if (el.classList.contains('is-open')) {
this.closeElement(el);
} else {
this.openElement(el);
}
this.syncAria(controlId);
}
close(event) {
// Close button inside a sidebar
const container = event.currentTarget.closest('nav, aside');
if (container) {
this.closeElement(container);
this.syncAria(container.id);
}
}
openElement(el) {
// Only apply overlay behavior on mobile
if (this.isDesktop()) return; // grid already shows them
el.classList.add('is-open');
document.body.classList.add('no-scroll');
}
closeElement(el) {
el.classList.remove('is-open');
if (!this.anyOpen()) {
document.body.classList.remove('no-scroll');
}
}
anyOpen() {
return !!document.querySelector('nav.is-open, aside.is-open');
}
syncAria(id) {
// Update any toggle buttons that control this id
const expanded = document.getElementById(id)?.classList.contains('is-open') || false;
document.querySelectorAll(`[aria-controls="${id}"]`).forEach(btn => {
btn.setAttribute('aria-expanded', expanded.toString());
});
}
handleResize() {
if (this.isDesktop()) {
// Ensure both sidebars are visible in desktop layout
['leftNav', 'rightNav'].forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.remove('is-open');
this.syncAria(id);
});
document.body.classList.remove('no-scroll');
} else {
// On mobile ensure aria-expanded is false unless explicitly opened
['leftNav', 'rightNav'].forEach(id => this.syncAria(id));
}
}
handleKeydown(e) {
if (e.key === 'Escape') {
const open = document.querySelectorAll('nav.is-open, aside.is-open');
if (open.length) {
open.forEach(el => this.closeElement(el));
['leftNav', 'rightNav'].forEach(id => this.syncAria(id));
}
}
}
handleDocumentClick(e) {
if (this.isDesktop()) return; // only needed mobile
const open = document.querySelectorAll('nav.is-open, aside.is-open');
if (!open.length) return;
const inside = e.target.closest('nav, aside, .mobile-toggles');
if (!inside) {
open.forEach(el => this.closeElement(el));
['leftNav', 'rightNav'].forEach(id => this.syncAria(id));
}
}
isDesktop() {
return this.mediaQuery.matches;
}
}

27
assets/styles/app.css

@ -27,7 +27,14 @@ h1 { @@ -27,7 +27,14 @@ h1 {
h1.brand {
font-family: var(--brand-font), serif;
font-size: 3.6rem;
color: var(--color-primary);
font-size: 3.2rem;
}
@media screen and (max-width: 600px) {
h1.brand {
font-size: 2.3rem;
}
}
h1.brand a {
@ -38,6 +45,11 @@ h2 { @@ -38,6 +45,11 @@ h2 {
font-size: 2.2rem;
}
h2.brand {
font-family: var(--brand-font), serif;
color: var(--color-primary);
}
h3 {
font-size: 2rem;
}
@ -262,10 +274,19 @@ div:nth-child(odd) .featured-list { @@ -262,10 +274,19 @@ div:nth-child(odd) .featured-list {
z-index: 1000; /* Ensure it stays on top */
display: flex;
flex-direction: column;
justify-content: space-around;
justify-content: space-between;
align-items: center;
background-color: var(--color-bg); /* Black background */
border-bottom: 1px solid var(--color-border); /* White bottom border */
border-bottom: 1px solid var(--color-border);
}
.header .container {
display: flex;
flex-direction: column;
align-items: start;
width: 100%;
max-width: 1200px;
padding: 0 20px;
}
.header__categories ul {

6
assets/styles/button.css

@ -22,6 +22,12 @@ button:active, .btn:active { @@ -22,6 +22,12 @@ button:active, .btn:active {
border-color: var(--color-text);
}
a.btn, a.btn:hover, a.btn:active {
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn.btn-secondary {
color: var(--color-secondary);
background-color: var(--color-bg);

43
assets/styles/form.css

@ -1,28 +1,43 @@ @@ -1,28 +1,43 @@
form, form > div {
form {
display: flex;
flex-direction: column;
clear: both;
margin-bottom: 1em;
}
label {
form > div:not(.actions) {
display: flex;
clear: both;
flex-direction: column;
margin-bottom: 1.5em;
}
input {
label {
display: flex;
clear: both;
}
input, textarea, select {
display: block;
clear: both;
}
input, textarea, select, .quill {
background-color: var(--color-bg);
color: var(--color-text);
border: 2px solid var(--color-border);
padding: 10px;
border: 1px solid var(--color-primary);
border-radius: 0; /* Sharp edges */
}
input, textarea, select {
padding: 10px;
}
textarea, input, select {
font-family: var(--font-family), sans-serif;
font-size: 1rem;
line-height: 1.5;
}
input:focus, textarea:focus, select:focus {
border-color: var(--color-primary);
outline: none;
@ -56,14 +71,6 @@ input:focus, textarea:focus, select:focus { @@ -56,14 +71,6 @@ input:focus, textarea:focus, select:focus {
flex-grow: 1;
}
textarea, input {
font-family: var(--font-family), sans-serif;
}
.quill {
border: 2px solid var(--color-border);
}
#editor {
margin: 0;
}
@ -72,3 +79,11 @@ button:disabled { @@ -72,3 +79,11 @@ button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
fieldset {
border: none;
}
.actions {
margin-bottom: 1.5em;
}

72
assets/styles/landing.css

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
.center{ text-align: center; }
/* Eyebrow + lede */
.eyebrow{
text-transform: uppercase;
letter-spacing: .12em;
font-size: .8rem;
color: var(--color-text-mid);
margin: 0 0 1rem;
}
/* Hero split */
.ln-hero{
margin-top: 100px;
background: var(--color-bg);
color: var(--color-text);
}
.ln-hero__copy{ }
.ln-hero__frame{ display:flex; justify-content:center; }
.frame{
width: min(560px, 40vw);
border-radius: 14px;
border: var(--border-hair);
background: linear-gradient(180deg, color-mix(in oklab, var(--color-bg-light) 80%, var(--color-bg)), var(--color-bg));
overflow: hidden;
}
/* Generic section shell */
.ln-section{ position: relative; padding: 3.2rem 0; }
/* Split layout for features */
.ln-split{
display: grid; gap: var(--gutter);
grid-template-columns: 280px 1fr;
align-items: start;
}
.ln-split__aside{ position: sticky; top: 24px; align-self: start; }
.ln-split__body .measure{ max-width: 70ch; }
.cta-row{ margin-top: .6rem; }
/* Section palettes (alternating) */
.ln-section--search{
background: var(--color-accent-300);
}
.ln-section--newsstand{
background: color-mix(in oklab, var(--color-primary) 18%, var(--color-bg));
}
.ln-section--reader{
background: var(--color-bg-light);
}
.ln-section--editor{
background: var(--color-teal-400);
}
.ln-section--newsroom{
background: color-mix(in oklab, var(--color-bg-light) 82%, var(--color-bg));
}
.ln-section--marketplace{
background: color-mix(in oklab, var(--color-primary) 18%, var(--color-bg));
}
.ln-section--unfold{
background: color-mix(in oklab, var(--color-accent-warm) 60%, var(--color-bg));
}
/* Motion */
@media (prefers-reduced-motion: reduce){
*{ transition: none !important; animation: none !important; }
}

162
assets/styles/layout.css

@ -9,27 +9,40 @@ @@ -9,27 +9,40 @@
* - Footer (footer)
**/
/* Layout Container */
.layout {
max-width: 100%;
width: 1200px;
width: 100%;
margin: 0 auto;
display: flex;
flex-grow: 1;
display: grid;
grid-template-columns: 200px auto 200px;
}
nav, aside {
position: sticky;
top: 70px;
}
nav {
width: 21vw;
min-width: 150px;
max-width: 280px;
flex-shrink: 0;
padding: 1em;
overflow-y: auto; /* Ensure the menu is scrollable if content is too long */
}
header {
position: fixed;
width: 100vw;
min-height: 60px;
top: 0;
left: 0;
}
.header__logo {
display: flex;
width: 100%;
margin-left: 10px;
}
nav ul {
list-style-type: none;
padding: 0;
@ -49,19 +62,6 @@ nav a:hover { @@ -49,19 +62,6 @@ nav a:hover {
text-decoration: none;
}
header {
position: fixed;
width: 100vw;
top: 0;
left: 0;
}
.header__logo {
display: flex;
width: 100%;
justify-content: center;
}
#progress-bar {
position: absolute;
left: 0;
@ -75,16 +75,33 @@ header { @@ -75,16 +75,33 @@ header {
/* Main content */
main {
display: flex;
flex-direction: column;
margin-top: 90px;
flex-grow: 1;
padding: 1em;
padding: 0 1em;
word-break: break-word;
}
main.static {
margin-left: auto;
margin-right: auto;
max-width: 1000px;
}
.w-container {
max-width: 1000px;
margin-left: auto;
margin-right: auto;
width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
}
.user-menu {
position: fixed;
top: 100px;
width: calc(21vw - 10px);
min-width: 150px;
max-width: 270px;
}
@ -96,9 +113,10 @@ main { @@ -96,9 +113,10 @@ main {
/* Right sidebar */
aside {
margin-top: 90px;
width: 300px;
padding: 1em;
display: flex;
flex-direction: column;
margin-top: 80px;
padding: 0 1em;
}
table {
@ -121,14 +139,93 @@ dt { @@ -121,14 +139,93 @@ dt {
margin-top: 10px;
}
.mobile-toggles {
display: none;
}
nav header,
aside header {
display: none;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.layout {
grid-template-columns: auto;
}
nav header,
aside header {
display: block;
}
nav, aside {
display: none; /* Hide the sidebars on small screens */
}
main {
margin-top: 90px;
width: 100%;
.mobile-toggles {
display: flex;
justify-content: space-between;
gap: .5rem;
position: sticky;
top: 61px; /* below main header */
background: var(--color-bg);
z-index: 1050;
padding: .5rem 0;
}
.mobile-toggles .toggle {
background: var(--color-primary);
color: var(--color-text-contrast);
border: none;
padding: .4rem .75rem;
cursor: pointer;
font: inherit;
}
nav.is-open, aside.is-open {
display: block;
position: fixed;
top: 0;
bottom: 0;
width: 80%;
max-width: 260px;
background: var(--color-bg);
box-shadow: 0 0 12px rgba(0,0,0,.4);
overflow-y: auto;
z-index: 1200;
padding: 90px 1em 1em; /* space for global header height */
animation: slideIn .25s ease;
}
nav.is-open { left: 0; }
aside.is-open { right: 0; }
body.no-scroll { overflow: hidden; }
nav.is-open > header, aside.is-open > header {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: flex-end;
padding: .5rem;
background: var(--color-bg);
border-bottom: 1px solid var(--color-border);
}
nav.is-open button.close, aside.is-open button.close {
background: transparent;
border: none;
font-size: 1.2rem;
cursor: pointer;
color: var(--color-text);
}
@keyframes slideIn {
from { transform: translateX(-15px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
}
@ -149,3 +246,8 @@ footer .footer-links { @@ -149,3 +246,8 @@ footer .footer-links {
.search input {
flex-grow: 1;
}
nav > header, aside > header { /* prevent global header fixed rules applying to nested headers */
position: static;
width: auto;
}

13
assets/styles/theme.css

@ -19,6 +19,19 @@ @@ -19,6 +19,19 @@
--heading-font: 'EB Garamond', serif; /* Set the font for headings */
--brand-font: 'Lobster', serif; /* A classic, refined branding font */
--brand-color: white;
--color-accent: #8FCB7E; /* fresh moss (main accent) */
--color-accent-strong: #B98BDC; /* lilac pop for headings/CTAs */
--color-accent-teal: #78C8BD; /* teal for tags/pills */
--color-accent-warm: #E1B574; /* warm highlight (badges/notes) */
--color-accent-600: #7FBF70;
--color-accent-500: #8FCB7E;
--color-accent-400: #A5D692;
--color-accent-300: #BCE3A9;
--color-teal-500: #78C8BD;
--color-teal-400: #8ED5CC;
--color-lilac-500: #B98BDC;
--color-lilac-400: #C7A1E3;
}
[data-theme="light"] {

6
assets/styles/utilities.css

@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}
.mx-0{margin-left:0!important;margin-right:0!important}.mx-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-3{margin-left:1rem!important;margin-right:1rem!important}.mx-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-5{margin-left:3rem!important;margin-right:3rem!important}
.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}
.mx-auto{margin-left:auto!important;margin-right:auto!important}
/* Display & layout */
.d-flex{display:flex!important;flex-direction:column}
.d-inline{display:inline!important}
@ -16,6 +16,10 @@ @@ -16,6 +16,10 @@
.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}
.flex-row{flex-direction:row}
.justify-content-between{justify-content:space-between!important}
.justify-content-center{justify-content:center!important}
.align-items-center{align-items:center!important}
.align-items-start{align-items:flex-start!important}
/* Lists */
.list-unstyled{list-style:none;padding-left:0;margin:0}

3
config/packages/twig.yaml

@ -1,5 +1,8 @@ @@ -1,5 +1,8 @@
twig:
file_name_pattern: '*.twig'
globals:
project_npub: 'npub1ez09adke4vy8udk3y2skwst8q5chjgqzym9lpq4u58zf96zcl7kqyry2lz'
dev_npub: 'npub1636uujeewag8zv8593lcvdrwlymgqre6uax4anuq3y5qehqey05sl8qpl4'
when@test:
twig:

1
config/packages/web_profiler.yaml

@ -7,3 +7,4 @@ when@local: @@ -7,3 +7,4 @@ when@local:
profiler:
only_exceptions: false
collect_serializer_data: true
collect: false

9
src/Controller/Administration/MagazineAdminController.php

@ -114,15 +114,6 @@ class MagazineAdminController extends AbstractController @@ -114,15 +114,6 @@ class MagazineAdminController extends AbstractController
];
}
// Sort alphabetically
usort($magazines, fn($a, $b) => strcmp($a['name'], $b['name']));
foreach ($magazines as &$mag) {
usort($mag['categories'], fn($a, $b) => strcmp($a['name'], $b['name']));
foreach ($mag['categories'] as &$cat) {
usort($cat['files'], fn($a, $b) => strcmp($a['name'], $b['name']));
}
}
return $this->render('admin/magazines.html.twig', [
'magazines' => $magazines,
]);

72
src/Controller/DefaultController.php

@ -4,6 +4,7 @@ declare(strict_types=1); @@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller;
use Elastica\Collapse;
use Elastica\Query;
use Elastica\Query\Terms;
use Exception;
@ -36,8 +37,11 @@ class DefaultController extends AbstractController @@ -36,8 +37,11 @@ class DefaultController extends AbstractController
$item->expiresAfter(13600); // about 4 hours
// get latest articles
$q = new Query();
$q->setSize(50);
$q->setSize(12);
$q->setSort(['createdAt' => ['order' => 'desc']]);
$col = new Collapse();
$col->setFieldname('pubkey');
$q->setCollapse($col);
return $this->finder->find($q);
});
@ -46,12 +50,62 @@ class DefaultController extends AbstractController @@ -46,12 +50,62 @@ class DefaultController extends AbstractController
]);
}
/**
* @throws Exception
*/
#[Route('/newsstand', name: 'newsstand')]
public function newsstand(): Response
{
return $this->render('pages/newsstand.html.twig');
}
/**
* @throws Exception
*/
#[Route('/lists', name: 'lists')]
public function lists(): Response
{
return $this->render('pages/lists.html.twig');
}
/**
* @throws InvalidArgumentException
*/
#[Route('/latest', name: 'latest')]
public function latest() : Response
{
$cacheKey = 'home-latest-articles';
$latest = $this->redisCache->get($cacheKey, function (ItemInterface $item) {
$item->expiresAfter(13600); // about 4 hours
// get latest articles
$q = new Query();
$q->setSize(12);
$q->setSort(['createdAt' => ['order' => 'desc']]);
$col = new Collapse();
$col->setFieldname('pubkey');
$q->setCollapse($col);
return $this->finder->find($q);
});
return $this->render('home.html.twig', [
'latest' => $latest
]);
}
/**
* @throws InvalidArgumentException
*/
#[Route('/mag/{mag}', name: 'magazine-index')]
public function magIndex($mag) : Response
{
return new Response('Not implemented yet', 501);
}
/**
* @throws InvalidArgumentException
*/
#[Route('/cat/{slug}', name: 'magazine-category')]
public function magCategory($slug, CacheInterface $redisCache,
#[Route('/mag/{mag}/cat/{slug}', name: 'magazine-category')]
public function magCategory($mag, $slug, CacheInterface $redisCache,
FinderInterface $finder,
LoggerInterface $logger): Response
{
@ -163,6 +217,18 @@ class DefaultController extends AbstractController @@ -163,6 +217,18 @@ class DefaultController extends AbstractController
]);
}
/**
* @throws InvalidArgumentException
*/
#[Route('/list/{slug}', name: 'reading-list')]
public function readingList($slug, CacheInterface $redisCache,
FinderInterface $finder,
LoggerInterface $logger): Response
{
return new Response('Not implemented yet', 501);
}
/**
* OG Preview endpoint for URLs
*/

18
src/Controller/StaticController.php

@ -39,4 +39,22 @@ class StaticController extends AbstractController @@ -39,4 +39,22 @@ class StaticController extends AbstractController
{
return $this->render('static/manifest.webmanifest.twig', [], new Response('', 200, ['Content-Type' => 'application/manifest+json']));
}
#[Route('/landing', name: 'landing')]
public function landing(): Response
{
return $this->render('static/landing.html.twig');
}
#[Route('/unfold', name: 'unfold')]
public function unfold(): Response
{
return $this->render('static/unfold.html.twig');
}
#[Route('/journals', name: 'journals_index')]
public function journalsIndex(): Response
{
return $this->render('pages/journals.html.twig');
}
}

4
src/Entity/Event.php

@ -115,8 +115,8 @@ class Event @@ -115,8 +115,8 @@ class Event
public function getTitle(): ?string
{
foreach ($this->getTags() as $tag) {
if (array_key_first($tag) === 'title') {
return $tag['title'];
if ($tag[0] === 'title') {
return $tag[1];
}
}
return null;

18
src/Twig/Components/Molecules/CategoryLink.php

@ -10,6 +10,7 @@ final class CategoryLink @@ -10,6 +10,7 @@ final class CategoryLink
{
public string $title;
public string $slug;
public ?string $mag = null; // magazine slug passed from parent (optional)
public function __construct(private CacheInterface $redisCache)
{
@ -18,21 +19,28 @@ final class CategoryLink @@ -18,21 +19,28 @@ final class CategoryLink
public function mount($coordinate): void
{
if (key_exists(1, $coordinate)) {
$parts = explode(':', $coordinate[1]);
$this->slug = $parts[2];
$cat = $this->redisCache->get('magazine-' . $parts[2], function (){
$parts = explode(':', $coordinate[1], 3);
// Expect format kind:pubkey:slug
$this->slug = $parts[2] ?? '';
$cat = $this->redisCache->get('magazine-' . $this->slug, function (){
return null;
});
if ($cat === null) {
$this->title = $this->slug ?: 'Category';
return;
}
$tags = $cat->getTags();
$title = array_filter($tags, function($tag) {
return ($tag[0] === 'title');
});
$this->title = $title[array_key_first($title)][1];
$this->title = $title[array_key_first($title)][1] ?? ($this->slug ?: 'Category');
} else {
dump($coordinate);die();
$this->title = 'Category';
$this->slug = '';
}
}

38
src/Twig/Components/Organisms/ZineList.php

@ -12,26 +12,40 @@ use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; @@ -12,26 +12,40 @@ use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
final class ZineList
{
public array $nzines = [];
public array $indices = [];
public function __construct(private readonly EntityManagerInterface $entityManager)
{
}
public function mount(?array $nzines = null): void
public function mount(): 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);
$nzines = $this->entityManager->getRepository(Event::class)->findBy(['kind' => KindsEnum::PUBLICATION_INDEX]);
// filter, only keep type === magazine
$this->nzines = array_filter($nzines, function ($index) {
// look for tags
$tags = $index->getTags();
$isMagType = false;
$isTopLevel = false;
foreach ($tags as $tag) {
// only if tag 'type' with value 'magazine'
if ($tag[0] === 'type' && $tag[1] === 'magazine') {
$isMagType = true;
}
// and only contains other indices:
// a tags with kind 30040
if ($tag[0] === 'a' && $isTopLevel === false) {
// tag format: ['a', 'kind:pubkey:slug']
$parts = explode(':', $tag[1]);
if ($parts[0] == (string)KindsEnum::PUBLICATION_INDEX->value) {
$isTopLevel = true;
}
}
}
return $isMagType && $isTopLevel;
});
}
}

2
templates/admin/analytics.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block title %}Visitor Analytics{% endblock %}

2
templates/admin/articles.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>Latest 50 Articles</h1>

2
templates/admin/magazine_editor.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>Edit Index: {{ title }}</h1>

2
templates/admin/magazines.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>Magazines</h1>

2
templates/admin/roles.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>{{ 'heading.roles'|trans }}</h1>

2
templates/admin/transactions.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block title %}Credit Transactions{% endblock %}

18
templates/base.html.twig

@ -19,8 +19,7 @@ @@ -19,8 +19,7 @@
<meta property="og:image" content="{{ asset('icons/favicon.ico') }}" />
{% endblock %}
{% block stylesheets %}
{% endblock %}
{% block stylesheets %}{% endblock %}
{% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
@ -29,19 +28,7 @@ @@ -29,19 +28,7 @@
<twig:Header />
<div class="layout">
<nav>
<twig:UserMenu />
{% block nav %}{% endblock %}
</nav>
<main>
{% block body %}{% endblock %}
</main>
<aside>
{% block aside %}{% endblock %}
</aside>
</div>
{% block layout %}{% endblock %}
<div data-controller="install-prompt">
<div
@ -54,7 +41,6 @@ @@ -54,7 +41,6 @@
</div>
</div>
<footer>
<twig:Footer />
</footer>

2
templates/bundles/TwigBundle/Exception/error.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>Internal Server Error</h1>

2
templates/bundles/TwigBundle/Exception/error404.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>Page not found</h1>

4
templates/components/Header.html.twig

@ -1,6 +1,8 @@ @@ -1,6 +1,8 @@
<header class="header" data-controller="menu" {{ attributes }}>
<div class="container">
<div class="header__logo">
<h1 class="brand"><a href="/">newsroom</a></h1>
<h1 class="brand"><a href="/">Decent Newsroom</a></h1>
</div>
</div>
<div data-controller="progress-bar">
<div id="progress-bar" data-progress-bar-target="bar"></div>

9
templates/components/Molecules/CategoryLink.html.twig

@ -1,4 +1,9 @@ @@ -1,4 +1,9 @@
<a {% if path('magazine-category', { 'slug' : slug }) in app.request.uri %}class="active"{% endif %}
href="{{ path('magazine-category', { 'slug' : slug }) }}">
{% if mag is defined and mag and slug %}
{% set catPath = path('magazine-category', { 'mag': mag, 'slug': slug }) %}
{% else %}
{% set catPath = '#' %}
{% endif %}
<a {% if catPath in app.request.uri %}class="active"{% endif %}
href="{{ catPath }}">
{{ title }}
</a>

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

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
<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>
<a href="{{ path('reading-list', { slug: item.slug }) }}">{{ item.title }}</a>
<div><small class="text-muted">{{ item.createdAt|date('Y-m-d') }}</small></div>
</li>
{% endfor %}

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

@ -1,23 +1,40 @@ @@ -1,23 +1,40 @@
<div {{ attributes }}>
{% if nzines is not empty %}
{% for item in nzines %}
{% set idx = indices[item.npub] is defined ? indices[item.npub] : null %}
<a class="card" href="{{ path('nzine_view', { npub: item.npub })}}">
<div class="card-body">
<div class="card">
<div class="card-body text-center">
<h3 class="card-title">
{% if idx and idx.title %}
{{ idx.title }}
<a href="{{ path('magazine-index', {mag: item.slug}) }}">
{% if item and item.title %}
{{ item.title }}
{% elseif item.slug %}
{{ item.slug }}
{% else %}
{{ item.npub }}
Untitled Magazine
{% endif %}
</a>
</h3>
{% if idx and idx.summary %}
<p class="hidden">{{ idx.summary }}</p>
{% if item and item.summary %}
<p class="lede">{{ item.summary }}</p>
{% endif %}
{# Category links (from 'a' tags) #}
{% set categoryTags = [] %}
{% if item.tags is defined %}
{% for tag in item.tags %}
{% if tag[0] is defined and tag[0] == 'a' %}
{% set categoryTags = categoryTags|merge([tag]) %}
{% endif %}
{% endfor %}
{% endif %}
{% if categoryTags is not empty %}
<ul class="list-unstyled d-flex flex-row gap-3 justify-content-center mt-3">
{% for catTag in categoryTags %}
<li class="list-inline-item"><twig:Molecules:CategoryLink :coordinate="catTag" :mag="item.slug" /></li>
{% endfor %}
</ul>
{% endif %}
</div>
</a>
</div>
<br>
{% endfor %}
{% else %}

7
templates/components/UserMenu.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<div class="user-menu" {{ attributes.defaults(stimulus_controller('login')) }}>
<div {{ attributes.defaults(stimulus_controller('login')) }}>
{% if app.user %}
<div class="notice info">
<twig:Molecules:UserFromNpub ident="{{ app.user.npub }}" />
@ -17,14 +17,11 @@ @@ -17,14 +17,11 @@
<li>
<a href="{{ path('reading_list_index') }}">Compose List</a>
</li>
{% if is_granted('ROLE_EDITOR') %}
{% if is_granted('ROLE_ADMIN') %}
<li>
<a href="{{ path('mag_wizard_setup') }}">Create Magazine</a>
</li>
{% endif %}
<li>
<a href="{{ path('app_search_index') }}">{{ 'heading.search'|trans }}</a>
</li>
<li>
<a href="/logout" data-action="live#$render">{{ 'heading.logout'|trans }}</a>
</li>

2
templates/event/index.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block title %}Nostr Event{% endblock %}

111
templates/home.html.twig

@ -1,24 +1,103 @@ @@ -1,24 +1,103 @@
{% extends 'base.html.twig' %}
{% extends 'layout-full.html.twig' %}
{% block nav %}
{% endblock %}
{% block title %}Decent Newsroom — A decentralized platform for collaborative publishing{% endblock %}
{% block body %}
<div
data-controller="search-visibility"
data-action="search:changed@window->search-visibility#toggle"
>
<twig:SearchComponent :currentRoute="app.current_route" />
<div data-search-visibility-target="list">
<twig:Organisms:CardList :list="latest" />
<section class="ln-hero d-flex gap-3 center">
<h1 class="brand">Decent Newsroom</h1>
<p class="eyebrow">Collaborative publishing on Nostr</p>
<p class="lede mb-5">Explore, publish articles, and create magazines.</p>
</section>
<section class="d-flex gap-3 center ln-section--newsstand">
<div class="container mt-5">
<h1>Newsstand</h1>
<p class="eyebrow">for the digital age</p>
</div>
<div class="mb-5">
<p class="measure">Flip through a world of magazines in one place. Build your lineup, catch fresh releases, anywhere.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('newsstand') }}">Explore the rack</a>
</div>
{% endblock %}
</div>
</section>
<section class="d-flex gap-3 center ln-section--search">
<div class="container mt-5">
<h1>Discover</h1>
<p class="eyebrow">hidden gems</p>
</div>
<div class="mb-5">
<p class="measure">Our search is specialized for long-form Nostr content.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('app_search_index') }}">Find what you like</a>
</div>
</div>
</section>
<section class="d-flex gap-3 center ln-section--lists">
<div class="container mt-5">
<h1>Reading Lists</h1>
<p class="eyebrow">for collections, curations, courses and more</p>
</div>
<div class="mb-5">
<p class="measure">Create ordered reading lists. Add more articles, reorder, republish.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('reading_list_index') }}">Start a list</a>
</div>
</div>
</section>
<section class="d-flex gap-3 center ln-section--editor">
<div class="container mt-5">
<h1>Article Editor</h1>
<p class="eyebrow">for anyone</p>
</div>
<div class="mb-5">
<p class="measure">Write, revise, and preview with ease. Save drafts, keep notes, and publish.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('editor-create') }}">Write an article</a>
</div>
</div>
</section>
<section class="d-flex gap-3 center ln-section--newsroom">
<div class="container mt-5">
<h1>Newsroom</h1>
<p class="eyebrow">like no other</p>
</div>
<div class="mb-5">
<p class="measure">Create your own magazine, define categories, manage submissions, and collaborate with editors and contributors.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('mag_wizard_setup') }}">Create a magazine</a>
</div>
</div>
</section>
{% block aside %}
<h6>Magazines</h6>
<twig:Organisms:ZineList />
<section class="d-flex gap-3 center ln-section--unfold">
<div class="container mt-5">
<h1>Unfold</h1>
<p class="eyebrow">your magazine</p>
</div>
<div class="mb-5">
<p class="measure">Unfold is a companion web app. Any magazine created on Decent Newsroom can be plugged in,
styled and deployed to a custom domain.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('unfold') }}">Learn more</a>
</div>
</div>
</section>
<section class="d-flex gap-3 center ln-section--marketplace">
<div class="container mt-5">
<h1>Media Marketplace</h1>
<p class="eyebrow">Coming soon</p>
</div>
<div class="mb-5">
<p class="measure">Commission custom visuals, discover stock you can actually use, and keep media and text in one creative flow.</p>
</div>
</section>
<h6>Lists</h6>
<twig:Organisms:ReadingListList />
{% endblock %}

5
templates/layout-full.html.twig

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
{% extends 'base.html.twig' %}
{% block layout %}
{% block body %}{% endblock %}
{% endblock %}

41
templates/layout.html.twig

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
{% extends 'base.html.twig' %}
{% block layout %}
<div class="layout" data-controller="sidebar-toggle">
<div>
<nav id="leftNav">
<header>
<button class="close" data-action="click->sidebar-toggle#close" aria-label="Close left sidebar">✕</button>
</header>
<ul class="user-nav">
<li>
<a href="{{ path('newsstand') }}">Newsstand</a>
</li>
<li>
<a href="{{ path('lists') }}">Lists</a>
</li>
<li>
<a href="{{ path('app_search_index') }}">{{ 'heading.search'|trans }}</a>
</li>
</ul>
<twig:UserMenu />
{% block nav %}{% endblock %}
</nav>
</div>
<main>
<div class="mobile-toggles">
<button class="toggle" aria-controls="leftNav" aria-expanded="false" data-action="click->sidebar-toggle#toggle">☰ Left</button>
<button class="toggle" aria-controls="rightNav" aria-expanded="false" data-action="click->sidebar-toggle#toggle">Right ☰</button>
</div>
{% block body %}{% endblock %}
</main>
<div>
<aside id="rightNav">
<header>
<button class="close" data-action="click->sidebar-toggle#close" aria-label="Close right sidebar">✕</button>
</header>
{% block aside %}{% endblock %}
</aside>
</div>
</div>
{% endblock %}

14
templates/magazine/magazine_articles.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>Attach Articles</h1>
@ -6,9 +6,8 @@ @@ -6,9 +6,8 @@
{{ form_start(form) }}
{% for cat in form.categories %}
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">{{ form_row(cat.title) }}</h5>
<fieldset class="mb-3">
<div class="card-title mb-3">{{ form_row(cat.title) }}</div>
<div
{{ stimulus_controller('form-collection') }}
data-form-collection-index-value="{{ cat.articles|length > 0 ? cat.articles|last.vars.name + 1 : 0 }}"
@ -21,12 +20,11 @@ @@ -21,12 +20,11 @@
</ul>
<button class="btn btn-sm btn-secondary" type="button" {{ stimulus_action('form-collection', 'addCollectionElement') }}>Add coordinate</button>
</div>
</div>
</div>
</fieldset>
{% endfor %}
<div class="mt-3 d-flex gap-2">
<a class="btn btn-outline-secondary" href="{{ path('mag_wizard_cancel') }}">Cancel</a>
<div class="mt-3 d-flex flex-row gap-2">
<a class="btn btn-secondary" href="{{ path('mag_wizard_cancel') }}">Cancel</a>
<button class="btn btn-primary">Next: Review & Sign</button>
</div>

2
templates/magazine/magazine_review.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>Review & Sign</h1>

4
templates/magazine/magazine_setup.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>Create Magazine</h1>
@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
<button type="button" class="btn btn-secondary" {{ stimulus_action('form-collection', 'addCollectionElement') }}>Add category</button>
</div>
<div class="mt-3 d-flex gap-2">
<div class="mt-3 d-flex flex-row gap-2">
<a class="btn btn-outline-secondary" href="{{ path('mag_wizard_cancel') }}">Cancel</a>
<button class="btn btn-primary">Next: Attach articles</button>
</div>

2
templates/pages/article.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block ogtags %}
<meta property="og:title" content="{{ article.title }}">

2
templates/pages/author.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}

2
templates/pages/category.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block ogtags %}
<meta property="og:title" content="{{ category.title }} - Newsroom">

8
templates/pages/editor.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% form_theme form _self %}
@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
{% endblock %}
{% block body %}
<div class="w-container">
<div {{ stimulus_controller('nostr-publish', {
publishUrl: path('api-article-publish'),
csrfToken: csrf_token('nostr_publish')
@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
{{ form_row(form.content) }}
{{ form_row(form.image) }}
<div data-controller="image-upload">
<div class="actions" data-controller="image-upload">
<button type="button"
class="btn btn-secondary"
data-action="click->image-upload#openDialog">
@ -80,5 +80,5 @@ @@ -80,5 +80,5 @@
{{ form_end(form) }}
</div>
</div>
{% endblock %}

0
templates/pages/journals.html.twig

5
templates/pages/latest.html.twig

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
{% extends 'layout.html.twig' %}
{% block body %}
<twig:Organisms:CardList :list="latest" />
{% endblock %}

14
templates/pages/lists.html.twig

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
{% extends 'layout.html.twig' %}
{% block body %}
<section class="d-flex gap-3 center ln-section--newsstand">
<div class="container mt-5 mb-5">
<h1>Reading Lists</h1>
<p class="eyebrow">for collections, curations, courses and more</p>
</div>
</section>
<section class="mb-5">
<twig:Organisms:ReadingListList />
</section>
{% endblock %}

2
templates/pages/magazine.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block ogtags %}
<meta property="og:title" content="{{ magazine.title }}">

18
templates/pages/newsstand.html.twig

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
{% extends 'layout.html.twig' %}
{% block body %}
<section class="d-flex gap-3 center ln-section--newsstand">
<div class="container mt-5 mb-5">
<h1>Newsstand</h1>
<p class="eyebrow">for the digital age</p>
</div>
</section>
<section class="mb-5">
<twig:Organisms:ZineList />
</section>
{% endblock %}
{% block aside %}
<h6>Lists</h6>
<twig:Organisms:ReadingListList />
{% endblock %}

2
templates/pages/nzine-editor.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
{% if nzine is not defined %}

2
templates/pages/nzine.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<div>

8
templates/pages/search.html.twig

@ -1,8 +1,14 @@ @@ -1,8 +1,14 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block nav %}
{% endblock %}
{% block body %}
<section class="d-flex gap-3 center ln-section--newsstand mb-5">
<div class="container mb-5 mt-5">
<h1>Discover</h1>
<p class="eyebrow">hidden gems</p>
</div>
</section>
<twig:SearchComponent />
{% endblock %}

2
templates/reading_list/compose.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>Compose Reading List</h1>

18
templates/reading_list/index.html.twig

@ -1,8 +1,15 @@ @@ -1,8 +1,15 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<section class="d-flex gap-3 center ln-section--newsstand">
<div class="container mt-5 mb-1">
<h1>Your Reading Lists</h1>
<p>Create and share curated reading lists.</p>
<p class="eyebrow">create and share curated reading lists</p>
</div>
<div class="cta-row mb-5">
<a class="btn btn-primary" href="{{ path('read_wizard_setup') }}">Create new</a>
</div>
</section>
{% if lists is defined and lists|length %}
<ul class="list-unstyled d-grid gap-2 mb-4">
@ -17,9 +24,9 @@ @@ -17,9 +24,9 @@
<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>
<a class="btn btn-sm btn-outline-primary" href="{{ path('reading-list', { 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>
<span class="hidden" data-copy-to-clipboard-target="textToCopy">{{ absolute_url(path('reading-list', { 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>
@ -34,7 +41,4 @@ @@ -34,7 +41,4 @@
<p><small>No reading lists found.</small></p>
{% endif %}
<div class="d-flex flex-row gap-2">
<a class="btn btn-primary" href="{{ path('read_wizard_setup') }}">Create a Reading List</a>
</div>
{% endblock %}

2
templates/reading_list/reading_articles.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>Attach Articles</h1>

2
templates/reading_list/reading_review.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>Review & Sign Reading List</h1>

2
templates/reading_list/reading_setup.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block body %}
<h1>Create Reading List</h1>

47
templates/static/about.html.twig

@ -1,36 +1,43 @@ @@ -1,36 +1,43 @@
{% extends 'base.html.twig' %}
{% block nav %}
{% endblock %}
{% extends 'layout-full.html.twig' %}
{% block body %}
<main class="d-flex gap-3 static">
<h1>About Decent Newsroom</h1>
<section>
<h2>Rebuilding Trust in Journalism</h2>
<p>The internet, and especially social media, made news instant and abundant, but also cheap and unreliable. The value of simply reporting what happened and where has dropped to zero,
buried under waves of misinformation, clickbait, and AI-generated noise. Worse, sorting truth from falsehood now costs more than spreading lies.</p>
<p>Decent Newsroom is our answer to this crisis. We are building a curated, decentralized magazine featuring high-quality, long-form journalism published on Nostr.</p>
</section>
<h2>How It Works</h2>
<dl>
<dt><strong>Curated Excellence</strong></dt>
<dd>We showcase a selection of featured articles.</dd>
<dt><strong>Indexed & Searchable</strong></dt>
<dd>Every article in Decent Newsroom is easily discoverable, improving access for readers and exposure for authors.</dd>
<dd>Simple search is available to logged-in users.</dd>
<dd>Semantic search is in development.</dd>
<dt><strong>Open to Writers & Publishers</strong> <span class="badge">Soon</span></dt>
<dd>Content creators can request indexing for their work, making it searchable and eligible for inclusion.</dd>
<dd>Publishers can create and manage their own magazines.</dd>
</dl>
<section>
<h2>Why It Matters</h2>
<p>The age of the newsroom isn’t over—it’s just evolving. We believe in bringing back editorial standards, collaboration, and high-value reporting in a decentralized way. Decent Newsroom is here to cut through the noise and rebuild trust in digital journalism.</p>
</section>
<br>
<section class="mb-5">
<p>To support us and get early access to the future of publishing, visit our crowdfunding page <a href="https://geyser.fund/project/newsroom" target="_blank">Geyser fund - Newsroom</a>.</p>
<br>
<p class="measure">For more info reach out to
<twig:Molecules:UserFromNpub ident="{{ project_npub }}" /> or
<twig:Molecules:UserFromNpub ident="{{ dev_npub }}" />.
</p>
</section>
</main>
<section class="d-flex gap-3 center ln-section--newsstand">
<div class="container mt-5">
<h1>Newsstand</h1>
<p class="eyebrow">for the digital age</p>
</div>
<div class="mb-5">
<p class="measure">Flip through a world of magazines in one place. Build your lineup, catch fresh releases, anywhere.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('newsstand') }}">Explore the rack</a>
</div>
</div>
</section>
{% endblock %}

103
templates/static/landing.html.twig

@ -0,0 +1,103 @@ @@ -0,0 +1,103 @@
{% extends 'layout-full.html.twig' %}
{% block title %}Decent Newsroom — A decentralized platform for collaborative publishing{% endblock %}
{% block body %}
<section class="ln-hero d-flex gap-3 center">
<h1 class="brand">Decent Newsroom</h1>
<p class="eyebrow">Collaborative publishing on Nostr</p>
<p class="lede mb-5">Explore, publish articles, and create magazines.</p>
</section>
<section class="d-flex gap-3 center ln-section--newsstand">
<div class="container mt-5">
<h1>Newsstand</h1>
<p class="eyebrow">for the digital age</p>
</div>
<div class="mb-5">
<p class="measure">Flip through a world of magazines in one place. Build your lineup, catch fresh releases, anywhere.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('newsstand') }}">Explore the rack</a>
</div>
</div>
</section>
<section class="d-flex gap-3 center ln-section--search">
<div class="container mt-5">
<h1>Discover</h1>
<p class="eyebrow">hidden gems</p>
</div>
<div class="mb-5">
<p class="measure">Our search is specialized for long-form Nostr content.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('app_search_index') }}">Find what you like</a>
</div>
</div>
</section>
<section class="d-flex gap-3 center ln-section--lists">
<div class="container mt-5">
<h1>Reading Lists</h1>
<p class="eyebrow">for collections, curations, courses and more</p>
</div>
<div class="mb-5">
<p class="measure">Create ordered reading lists. Add more articles, reorder, republish.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('reading_list_index') }}">Browse</a>
</div>
</div>
</section>
<section class="d-flex gap-3 center ln-section--editor">
<div class="container mt-5">
<h1>Article Editor</h1>
<p class="eyebrow">for anyone</p>
</div>
<div class="mb-5">
<p class="measure">Write, revise, and preview with ease. Save drafts, keep notes, and publish.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('editor-create') }}">Write an article</a>
</div>
</div>
</section>
<section class="d-flex gap-3 center ln-section--newsroom">
<div class="container mt-5">
<h1>Newsroom</h1>
<p class="eyebrow">like no other</p>
</div>
<div class="mb-5">
<p class="measure">Create your own magazine, define categories, manage submissions, and collaborate with editors and contributors.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('mag_wizard_setup') }}">Create a magazine</a>
</div>
</div>
</section>
<section class="d-flex gap-3 center ln-section--unfold">
<div class="container mt-5">
<h1>Unfold</h1>
<p class="eyebrow">your magazine</p>
</div>
<div class="mb-5">
<p class="measure">Unfold is a companion web app. Any magazine created on Decent Newsroom can be plugged in,
styled and deployed to a custom domain.</p>
<div class="cta-row">
<a class="btn btn--primary" href="{{ path('unfold') }}">Learn more</a>
</div>
</div>
</section>
<section class="d-flex gap-3 center ln-section--marketplace">
<div class="container mt-5">
<h1>Media Marketplace</h1>
<p class="eyebrow">Coming soon</p>
</div>
<div class="mb-5">
<p class="measure">Commission custom visuals, discover stock you can actually use, and keep media and text in one creative flow.</p>
</div>
</section>
{% endblock %}

2
templates/static/pricing.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block nav %}
{% endblock %}

2
templates/static/roadmap.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block nav %}
{% endblock %}

2
templates/static/tos.html.twig

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
{% extends 'base.html.twig' %}
{% extends 'layout.html.twig' %}
{% block nav %}
{% endblock %}

45
templates/static/unfold.html.twig

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
{% extends 'layout-full.html.twig' %}
{% block title %}Decent Newsroom — A decentralized platform for collaborative publishing{% endblock %}
{% block body %}
<section class="ln-hero d-flex gap-3 center">
<h1 class="brand">Unfold</h1>
<p class="eyebrow">by Decent Newsroom</p>
<p class="lede mb-5">Your magazine, your way.</p>
</section>
<section class="d-flex gap-3 center ln-section--unfold">
<div class="container mt-5">
<h2>Unfold is a skin for your magazine</h2>
</div>
<div class="mb-5">
<p class="measure">Any magazine created on Decent Newsroom can be plugged in,
styled according to your wishes and deployed to a custom domain.</p>
</div>
</section>
<section class="d-flex gap-3 center ln-section--newsroom">
<div class="container mt-5">
<h2>Unfold Lite</h2>
</div>
<div class="mb-5">
<p class="measure">We also offer subdomains, if rolling out your own website feels daunting.</p>
</div>
</section>
<section class="d-flex gap-3 center ln-section--marketplace">
<div class="container mt-5">
<h2>Interested?</h2>
</div>
<div class="mb-5">
<p class="measure">Reach out to
<twig:Molecules:UserFromNpub ident="{{ project_npub }}" /> or
<twig:Molecules:UserFromNpub ident="{{ dev_npub }}" />.
</p>
</div>
</section>
{% endblock %}
Loading…
Cancel
Save