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';
import './styles/analytics.css'; import './styles/analytics.css';
import './styles/modal.css'; import './styles/modal.css';
import './styles/utilities.css'; import './styles/utilities.css';
import './styles/landing.css';
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉'); console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');

115
assets/controllers/sidebar_toggle_controller.js

@ -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 {
h1.brand { h1.brand {
font-family: var(--brand-font), serif; 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 { h1.brand a {
@ -38,6 +45,11 @@ h2 {
font-size: 2.2rem; font-size: 2.2rem;
} }
h2.brand {
font-family: var(--brand-font), serif;
color: var(--color-primary);
}
h3 { h3 {
font-size: 2rem; font-size: 2rem;
} }
@ -262,10 +274,19 @@ div:nth-child(odd) .featured-list {
z-index: 1000; /* Ensure it stays on top */ z-index: 1000; /* Ensure it stays on top */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-around; justify-content: space-between;
align-items: center; align-items: center;
background-color: var(--color-bg); /* Black background */ 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 { .header__categories ul {

6
assets/styles/button.css

@ -22,6 +22,12 @@ button:active, .btn:active {
border-color: var(--color-text); 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 { .btn.btn-secondary {
color: var(--color-secondary); color: var(--color-secondary);
background-color: var(--color-bg); background-color: var(--color-bg);

43
assets/styles/form.css

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

72
assets/styles/landing.css

@ -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 @@
* - Footer (footer) * - Footer (footer)
**/ **/
/* Layout Container */ /* Layout Container */
.layout { .layout {
max-width: 100%; max-width: 100%;
width: 1200px; width: 100%;
margin: 0 auto; margin: 0 auto;
display: flex;
flex-grow: 1; flex-grow: 1;
display: grid;
grid-template-columns: 200px auto 200px;
}
nav, aside {
position: sticky;
top: 70px;
} }
nav { nav {
width: 21vw;
min-width: 150px;
max-width: 280px;
flex-shrink: 0;
padding: 1em; padding: 1em;
overflow-y: auto; /* Ensure the menu is scrollable if content is too long */ 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 { nav ul {
list-style-type: none; list-style-type: none;
padding: 0; padding: 0;
@ -49,19 +62,6 @@ nav a:hover {
text-decoration: none; text-decoration: none;
} }
header {
position: fixed;
width: 100vw;
top: 0;
left: 0;
}
.header__logo {
display: flex;
width: 100%;
justify-content: center;
}
#progress-bar { #progress-bar {
position: absolute; position: absolute;
left: 0; left: 0;
@ -75,16 +75,33 @@ header {
/* Main content */ /* Main content */
main { main {
display: flex;
flex-direction: column;
margin-top: 90px; margin-top: 90px;
flex-grow: 1; flex-grow: 1;
padding: 1em; padding: 0 1em;
word-break: break-word; 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 { .user-menu {
position: fixed; position: fixed;
top: 100px; top: 100px;
width: calc(21vw - 10px);
min-width: 150px; min-width: 150px;
max-width: 270px; max-width: 270px;
} }
@ -96,9 +113,10 @@ main {
/* Right sidebar */ /* Right sidebar */
aside { aside {
margin-top: 90px; display: flex;
width: 300px; flex-direction: column;
padding: 1em; margin-top: 80px;
padding: 0 1em;
} }
table { table {
@ -121,14 +139,93 @@ dt {
margin-top: 10px; margin-top: 10px;
} }
.mobile-toggles {
display: none;
}
nav header,
aside header {
display: none;
}
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.layout {
grid-template-columns: auto;
}
nav header,
aside header {
display: block;
}
nav, aside { nav, aside {
display: none; /* Hide the sidebars on small screens */ display: none; /* Hide the sidebars on small screens */
} }
main {
margin-top: 90px; .mobile-toggles {
width: 100%; 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 {
.search input { .search input {
flex-grow: 1; 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 @@
--heading-font: 'EB Garamond', serif; /* Set the font for headings */ --heading-font: 'EB Garamond', serif; /* Set the font for headings */
--brand-font: 'Lobster', serif; /* A classic, refined branding font */ --brand-font: 'Lobster', serif; /* A classic, refined branding font */
--brand-color: white; --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"] { [data-theme="light"] {

6
assets/styles/utilities.css

@ -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} .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} .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} .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 */ /* Display & layout */
.d-flex{display:flex!important;flex-direction:column} .d-flex{display:flex!important;flex-direction:column}
.d-inline{display:inline!important} .d-inline{display:inline!important}
@ -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} .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} .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 */ /* Lists */
.list-unstyled{list-style:none;padding-left:0;margin:0} .list-unstyled{list-style:none;padding-left:0;margin:0}

3
config/packages/twig.yaml

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

1
config/packages/web_profiler.yaml

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

9
src/Controller/Administration/MagazineAdminController.php

@ -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', [ return $this->render('admin/magazines.html.twig', [
'magazines' => $magazines, 'magazines' => $magazines,
]); ]);

72
src/Controller/DefaultController.php

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use Elastica\Collapse;
use Elastica\Query; use Elastica\Query;
use Elastica\Query\Terms; use Elastica\Query\Terms;
use Exception; use Exception;
@ -36,8 +37,11 @@ class DefaultController extends AbstractController
$item->expiresAfter(13600); // about 4 hours $item->expiresAfter(13600); // about 4 hours
// get latest articles // get latest articles
$q = new Query(); $q = new Query();
$q->setSize(50); $q->setSize(12);
$q->setSort(['createdAt' => ['order' => 'desc']]); $q->setSort(['createdAt' => ['order' => 'desc']]);
$col = new Collapse();
$col->setFieldname('pubkey');
$q->setCollapse($col);
return $this->finder->find($q); return $this->finder->find($q);
}); });
@ -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 * @throws InvalidArgumentException
*/ */
#[Route('/cat/{slug}', name: 'magazine-category')] #[Route('/mag/{mag}/cat/{slug}', name: 'magazine-category')]
public function magCategory($slug, CacheInterface $redisCache, public function magCategory($mag, $slug, CacheInterface $redisCache,
FinderInterface $finder, FinderInterface $finder,
LoggerInterface $logger): Response LoggerInterface $logger): Response
{ {
@ -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 * OG Preview endpoint for URLs
*/ */

18
src/Controller/StaticController.php

@ -39,4 +39,22 @@ class StaticController extends AbstractController
{ {
return $this->render('static/manifest.webmanifest.twig', [], new Response('', 200, ['Content-Type' => 'application/manifest+json'])); 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
public function getTitle(): ?string public function getTitle(): ?string
{ {
foreach ($this->getTags() as $tag) { foreach ($this->getTags() as $tag) {
if (array_key_first($tag) === 'title') { if ($tag[0] === 'title') {
return $tag['title']; return $tag[1];
} }
} }
return null; return null;

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

@ -10,6 +10,7 @@ final class CategoryLink
{ {
public string $title; public string $title;
public string $slug; public string $slug;
public ?string $mag = null; // magazine slug passed from parent (optional)
public function __construct(private CacheInterface $redisCache) public function __construct(private CacheInterface $redisCache)
{ {
@ -18,21 +19,28 @@ final class CategoryLink
public function mount($coordinate): void public function mount($coordinate): void
{ {
if (key_exists(1, $coordinate)) { if (key_exists(1, $coordinate)) {
$parts = explode(':', $coordinate[1]); $parts = explode(':', $coordinate[1], 3);
$this->slug = $parts[2]; // Expect format kind:pubkey:slug
$cat = $this->redisCache->get('magazine-' . $parts[2], function (){ $this->slug = $parts[2] ?? '';
$cat = $this->redisCache->get('magazine-' . $this->slug, function (){
return null; return null;
}); });
if ($cat === null) {
$this->title = $this->slug ?: 'Category';
return;
}
$tags = $cat->getTags(); $tags = $cat->getTags();
$title = array_filter($tags, function($tag) { $title = array_filter($tags, function($tag) {
return ($tag[0] === 'title'); return ($tag[0] === 'title');
}); });
$this->title = $title[array_key_first($title)][1]; $this->title = $title[array_key_first($title)][1] ?? ($this->slug ?: 'Category');
} else { } 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;
final class ZineList final class ZineList
{ {
public array $nzines = []; public array $nzines = [];
public array $indices = [];
public function __construct(private readonly EntityManagerInterface $entityManager) 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) { $nzines = $this->entityManager->getRepository(Event::class)->findBy(['kind' => KindsEnum::PUBLICATION_INDEX]);
// find indices for each nzine
foreach ($this->nzines as $zine) { // filter, only keep type === magazine
$ids = $this->entityManager->getRepository(Event::class)->findBy(['pubkey' => $zine->getNpub(), 'kind' => KindsEnum::PUBLICATION_INDEX]); $this->nzines = array_filter($nzines, function ($index) {
$id = array_filter($ids, function($k) use ($zine) { // look for tags
return $k->getSlug() == $zine->getSlug(); $tags = $index->getTags();
}); $isMagType = false;
if ($id) { $isTopLevel = false;
$this->indices[$zine->getNpub()] = array_pop($id); 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 @@
{% extends 'base.html.twig' %} {% extends 'layout.html.twig' %}
{% block title %}Visitor Analytics{% endblock %} {% block title %}Visitor Analytics{% endblock %}

2
templates/admin/articles.html.twig

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

2
templates/admin/magazine_editor.html.twig

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

2
templates/admin/magazines.html.twig

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

2
templates/admin/roles.html.twig

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

2
templates/admin/transactions.html.twig

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

18
templates/base.html.twig

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

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

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

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

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

4
templates/components/Header.html.twig

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

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

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

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

@ -4,7 +4,7 @@
<ul class="list-unstyled small d-grid gap-2"> <ul class="list-unstyled small d-grid gap-2">
{% for item in items %} {% for item in items %}
<li> <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> <div><small class="text-muted">{{ item.createdAt|date('Y-m-d') }}</small></div>
</li> </li>
{% endfor %} {% endfor %}

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

@ -1,23 +1,40 @@
<div {{ attributes }}> <div {{ attributes }}>
{% if nzines is not empty %} {% if nzines is not empty %}
{% for item in nzines %} {% for item in nzines %}
{% set idx = indices[item.npub] is defined ? indices[item.npub] : null %} <div class="card">
<a class="card" href="{{ path('nzine_view', { npub: item.npub })}}"> <div class="card-body text-center">
<div class="card-body">
<h3 class="card-title"> <h3 class="card-title">
{% if idx and idx.title %} <a href="{{ path('magazine-index', {mag: item.slug}) }}">
{{ idx.title }} {% if item and item.title %}
{{ item.title }}
{% elseif item.slug %} {% elseif item.slug %}
{{ item.slug }} {{ item.slug }}
{% else %} {% else %}
{{ item.npub }} Untitled Magazine
{% endif %} {% endif %}
</a>
</h3> </h3>
{% if idx and idx.summary %} {% if item and item.summary %}
<p class="hidden">{{ idx.summary }}</p> <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 %} {% endif %}
</div> </div>
</a> </div>
<br> <br>
{% endfor %} {% endfor %}
{% else %} {% else %}

7
templates/components/UserMenu.html.twig

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

2
templates/event/index.html.twig

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

111
templates/home.html.twig

@ -1,24 +1,103 @@
{% extends 'base.html.twig' %} {% extends 'layout-full.html.twig' %}
{% block nav %} {% block title %}Decent Newsroom — A decentralized platform for collaborative publishing{% endblock %}
{% endblock %}
{% block body %} {% block body %}
<div
data-controller="search-visibility" <section class="ln-hero d-flex gap-3 center">
data-action="search:changed@window->search-visibility#toggle" <h1 class="brand">Decent Newsroom</h1>
> <p class="eyebrow">Collaborative publishing on Nostr</p>
<twig:SearchComponent :currentRoute="app.current_route" /> <p class="lede mb-5">Explore, publish articles, and create magazines.</p>
<div data-search-visibility-target="list"> </section>
<twig:Organisms:CardList :list="latest" />
<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>
<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>
{% 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 %} <section class="d-flex gap-3 center ln-section--unfold">
<h6>Magazines</h6> <div class="container mt-5">
<twig:Organisms:ZineList /> <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 %} {% endblock %}

5
templates/layout-full.html.twig

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

41
templates/layout.html.twig

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

2
templates/magazine/magazine_review.html.twig

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

4
templates/magazine/magazine_setup.html.twig

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

2
templates/pages/article.html.twig

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

2
templates/pages/author.html.twig

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

2
templates/pages/category.html.twig

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

8
templates/pages/editor.html.twig

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

0
templates/pages/journals.html.twig

5
templates/pages/latest.html.twig

@ -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 @@
{% 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 @@
{% extends 'base.html.twig' %} {% extends 'layout.html.twig' %}
{% block ogtags %} {% block ogtags %}
<meta property="og:title" content="{{ magazine.title }}"> <meta property="og:title" content="{{ magazine.title }}">

18
templates/pages/newsstand.html.twig

@ -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 @@
{% extends 'base.html.twig' %} {% extends 'layout.html.twig' %}
{% block body %} {% block body %}
{% if nzine is not defined %} {% if nzine is not defined %}

2
templates/pages/nzine.html.twig

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

8
templates/pages/search.html.twig

@ -1,8 +1,14 @@
{% extends 'base.html.twig' %} {% extends 'layout.html.twig' %}
{% block nav %} {% block nav %}
{% endblock %} {% endblock %}
{% block body %} {% 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 /> <twig:SearchComponent />
{% endblock %} {% endblock %}

2
templates/reading_list/compose.html.twig

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

18
templates/reading_list/index.html.twig

@ -1,8 +1,15 @@
{% extends 'base.html.twig' %} {% extends 'layout.html.twig' %}
{% block body %} {% block body %}
<section class="d-flex gap-3 center ln-section--newsstand">
<div class="container mt-5 mb-1">
<h1>Your Reading Lists</h1> <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 %} {% if lists is defined and lists|length %}
<ul class="list-unstyled d-grid gap-2 mb-4"> <ul class="list-unstyled d-grid gap-2 mb-4">
@ -17,9 +24,9 @@
<div class="d-flex flex-row gap-2"> <div class="d-flex flex-row gap-2">
<a class="btn btn-sm btn-primary" href="{{ path('reading_list_compose') }}">Open Composer</a> <a class="btn btn-sm btn-primary" href="{{ path('reading_list_compose') }}">Open Composer</a>
{% if item.slug %} {% 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 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" <button class="btn btn-sm btn-secondary"
data-copy-to-clipboard-target="copyButton" data-copy-to-clipboard-target="copyButton"
data-action="click->copy-to-clipboard#copyToClipboard">Copy link</button> data-action="click->copy-to-clipboard#copyToClipboard">Copy link</button>
@ -34,7 +41,4 @@
<p><small>No reading lists found.</small></p> <p><small>No reading lists found.</small></p>
{% endif %} {% 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 %} {% endblock %}

2
templates/reading_list/reading_articles.html.twig

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

2
templates/reading_list/reading_review.html.twig

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

2
templates/reading_list/reading_setup.html.twig

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

47
templates/static/about.html.twig

@ -1,36 +1,43 @@
{% extends 'base.html.twig' %} {% extends 'layout-full.html.twig' %}
{% block nav %}
{% endblock %}
{% block body %} {% block body %}
<main class="d-flex gap-3 static">
<h1>About Decent Newsroom</h1> <h1>About Decent Newsroom</h1>
<section>
<h2>Rebuilding Trust in Journalism</h2> <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, <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> 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> <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> <section>
<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>
<h2>Why It Matters</h2> <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> <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> <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 %} {% endblock %}

103
templates/static/landing.html.twig

@ -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 @@
{% extends 'base.html.twig' %} {% extends 'layout.html.twig' %}
{% block nav %} {% block nav %}
{% endblock %} {% endblock %}

2
templates/static/roadmap.html.twig

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

2
templates/static/tos.html.twig

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

45
templates/static/unfold.html.twig

@ -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