Browse Source

Make it a forum experience

imwald
Nuša Pukšič 3 months ago
parent
commit
03a9299757
  1. 1
      assets/app.js
  2. 4
      assets/styles/02-layout/layout.css
  3. 135
      assets/styles/04-pages/forum.css
  4. 17
      assets/styles/media-discovery.css
  5. 63
      src/Controller/ArticleController.php
  6. 256
      src/Controller/ForumController.php
  7. 5
      src/Controller/SearchController.php
  8. 12
      src/Twig/Components/Atoms/ForumAside.php
  9. 12
      src/Twig/Components/Atoms/PageHeading.php
  10. 192
      src/Util/ForumTopics.php
  11. 13
      templates/components/Atoms/ForumAside.html.twig
  12. 8
      templates/components/Atoms/PageHeading.html.twig
  13. 32
      templates/forum/index.html.twig
  14. 22
      templates/forum/tag.html.twig
  15. 22
      templates/forum/topic.html.twig
  16. 7
      templates/pages/latest-articles.html.twig
  17. 6
      templates/pages/media-discovery.html.twig
  18. 4
      templates/pages/search.html.twig

1
assets/app.js

@ -42,6 +42,7 @@ import './styles/04-pages/landing.css'; @@ -42,6 +42,7 @@ import './styles/04-pages/landing.css';
import './styles/04-pages/admin.css';
import './styles/04-pages/analytics.css';
import './styles/04-pages/author-media.css';
import './styles/04-pages/forum.css';
// 05 - Utilities (last for highest specificity)
import './styles/05-utilities/utilities.css';

4
assets/styles/02-layout/layout.css

@ -25,7 +25,9 @@ body { @@ -25,7 +25,9 @@ body {
nav, aside {
position: sticky;
top: 70px;
top: 80px;
max-height: calc(100vh - 80px);
overflow: auto;
}
nav {

135
assets/styles/04-pages/forum.css

@ -0,0 +1,135 @@ @@ -0,0 +1,135 @@
/* Forum Styles - Old School Forum Look */
.forum-nav {
list-style: none;
padding: 0;
margin: 0;
}
.forum-nav li {
margin: 0.5rem 0;
}
.forum-nav button {
cursor: pointer;
padding: 0.5rem;
font-weight: bold;
background: #e9e9e9;
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
text-align: left;
}
.forum-nav button:hover {
background: #ddd;
}
.forum-nav .content {
padding: 0.5rem;
border: 1px solid #ccc;
border-top: none;
background: #f9f9f9;
}
.forum-nav .content ul {
list-style: none;
padding: 0;
margin: 0;
}
.forum-nav .content ul li {
margin: 0.25rem 0;
}
.forum-nav .content ul li a {
display: block;
padding: 0.25rem 0.5rem;
text-decoration: none;
color: #333;
}
.forum-nav .content ul li a:hover {
background: #f0f0f0;
}
.subcategories-grid {
display: flex;
flex-direction: column;
gap: 1rem;
margin: 1rem 0;
}
.sub-card {
border: 1px solid var(--color-primary);
background: #fff;
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.sub-card h3 {
margin: 0 0 0.5rem 0;
}
.sub-card div {
display: flex;
justify-content: space-between;
align-items: center;
}
.sub-card .count {
align-self: flex-end;
flex-shrink: 0;
}
.articles-list {
list-style: none;
padding: 0;
}
.article-item {
border: 1px solid #ddd;
padding: 1rem;
margin: 1rem 0;
background: #fafafa;
}
.article-item h3 {
margin: 0 0 0.5rem 0;
}
.article-item p {
margin: 0.5rem 0;
}
.article-item small {
color: #666;
}
.nav-sub-card {
border: 1px solid #333;
background: #fff;
padding: 0.5rem;
margin: 0.5rem 0;
display: flex;
flex-direction: column;
box-shadow: 1px 1px 3px rgba(0,0,0,0.2);
}
.nav-sub-card h4 {
margin: 0 0 0.25rem 0;
font-size: 1rem;
}
.nav-sub-card .tags {
flex: 1;
}
.nav-sub-card .count {
font-size: 1.2rem;
font-weight: bold;
color: #333;
align-self: flex-end;
}

17
assets/styles/media-discovery.css

@ -1,22 +1,5 @@ @@ -1,22 +1,5 @@
/* Media Discovery Page Styles */
.discover-header {
text-align: center;
padding: 2rem 1rem;
max-width: 800px;
margin: 0 auto;
}
.discover-header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.discover-subtitle {
color: #666;
font-size: 1.1rem;
}
.error-message {
background: #f8d7da;
color: #721c24;

63
src/Controller/ArticleController.php

@ -358,68 +358,5 @@ class ArticleController extends AbstractController @@ -358,68 +358,5 @@ class ArticleController extends AbstractController
return $data;
}
#[Route('/topics', name: 'topics')]
public function topics(
Request $request,
#[Autowire(service: 'fos_elastica.finder.articles')] PaginatedFinderInterface $finder
): Response {
$topics = [
'bitcoin-crypto' => [
'name' => 'Bitcoin & Crypto',
'tags' => ['bitcoin', 'bitkoin', 'lightning', 'decentralization', 'freedom', 'privacy', 'sovereignty']
],
'nostr' => [
'name' => 'Nostr',
'tags' => ['nostr', 'grownostr']
],
'christianity' => [
'name' => 'Christianity',
'tags' => ['jesus', 'christian', 'christianity', 'bible', 'trustjesus', 'biblestr']
],
'travel' => [
'name' => 'Travel',
'tags' => ['travel', 'europe']
],
'photography' => [
'name' => 'Photography',
'tags' => ['photography']
],
'ai' => [
'name' => 'AI',
'tags' => ['ai']
],
'philosophy' => [
'name' => 'Philosophy',
'tags' => ['philosophy']
],
'art' => [
'name' => 'Art',
'tags' => ['art']
]
];
$selectedTopic = $request->query->get('topic');
$articles = [];
if ($selectedTopic && isset($topics[$selectedTopic])) {
$tags = $topics[$selectedTopic]['tags'];
$query = [
'size' => 10,
'sort' => [['createdAt' => ['order' => 'desc']]],
'query' => [
'terms' => [
'topics' => $tags
]
]
];
$results = $finder->find($query);
$articles = array_slice($results, 0, 10);
}
return $this->render('pages/topics.html.twig', [
'topics' => $topics,
'selectedTopic' => $selectedTopic,
'articles' => $articles,
]);
}
}

256
src/Controller/ForumController.php

@ -0,0 +1,256 @@ @@ -0,0 +1,256 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Util\ForumTopics;
use Elastica\Aggregation\Filters as FiltersAgg;
use Elastica\Query;
use Elastica\Query\BoolQuery;
use Elastica\Query\Term;
use Elastica\Query\Terms;
use FOS\ElasticaBundle\Finder\PaginatedFinderInterface;
use Pagerfanta\Pagerfanta;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Cache\CacheInterface;
class ForumController extends AbstractController
{
#[Route('/forum', name: 'forum')]
public function index(
#[Autowire(service: 'fos_elastica.index.articles')] \Elastica\Index $index,
CacheInterface $cache,
Request $request
): Response {
// Optional: small cache so we don’t hammer ES on every page view
//$categoriesWithCounts = $cache->get('forum.index.counts.v2', function (ItemInterface $item) use ($index) {
// $item->expiresAfter(30); // 30s is a nice compromise for “live enough”
$allTags = $this->flattenAllTags(ForumTopics::TOPICS); // ['tag' => true, ...]
$counts = $this->fetchTagCounts($index, array_keys($allTags)); // ['tag' => count]
// return $this->hydrateCategoryCounts(self::TOPICS, $counts);
//});
$categoriesWithCounts = $this->hydrateCategoryCounts(ForumTopics::TOPICS, $counts);
return $this->render('forum/index.html.twig', [
'topics' => $categoriesWithCounts,
]);
}
#[Route('/forum/topic/{key}', name: 'forum_topic')]
public function topic(
string $key,
#[Autowire(service: 'fos_elastica.finder.articles')] PaginatedFinderInterface $finder,
#[Autowire(service: 'fos_elastica.index.articles')] \Elastica\Index $index,
Request $request
): Response {
// key format: "{category}-{subcategory}"
$key = strtolower(trim($key));
[$cat, $sub] = array_pad(explode('-', $key, 2), 2, null);
if (!$cat || !$sub || !isset(ForumTopics::TOPICS[$cat]['subcategories'][$sub])) {
throw $this->createNotFoundException('Topic not found');
}
$topic = ForumTopics::TOPICS[$cat]['subcategories'][$sub];
// Count each tag in this subcategory in one shot
$tags = array_map('strval', $topic['tags']);
$tagCounts = $this->fetchTagCounts($index, $tags);
// Fetch articles for the topic
$bool = new BoolQuery();
$bool->addFilter(new Terms('topics', $tags));
$query = new Query($bool);
$query->setSize(20);
$query->setSort(['createdAt' => ['order' => 'desc']]);
/** @var Pagerfanta $pager */
$pager = $finder->findPaginated($query);
$pager->setMaxPerPage(20);
$pager->setCurrentPage(max(1, (int) $request->query->get('page', 1)));
$articles = iterator_to_array($pager->getCurrentPageResults());
// (Optional) also show latest threads under this topic scope
$page = max(1, (int) $request->query->get('page', 1));
$perPage = 20;
$threads = $this->fetchThreads($index, [$tags]); // OR scope: any tag in subcategory
$threadsPage = array_slice($threads, ($page-1)*$perPage, $perPage);
return $this->render('forum/topic.html.twig', [
'categoryKey' => $cat,
'subcategoryKey' => $sub,
'topic' => [
'name' => $topic['name'],
'tags' => $tags,
],
'tags' => $tagCounts, // ['tag' => count]
'threads' => $threadsPage,
'total' => count($threads),
'page' => $page,
'perPage' => $perPage,
'topics' => $this->getHydratedTopics($index),
'articles' => $articles
]);
}
#[Route('/forum/tag/{tag}', name: 'forum_tag')]
public function tag(
string $tag,
#[Autowire(service: 'fos_elastica.finder.articles')] PaginatedFinderInterface $finder,
#[Autowire(service: 'fos_elastica.index.articles')] \Elastica\Index $index,
Request $request
): Response {
$tag = strtolower(trim($tag));
$bool = new BoolQuery();
// Correct Term usage:
$bool->addFilter(new Term(['topics' => $tag]));
$query = new Query($bool);
$query->setSize(20);
$query->setSort(['createdAt' => ['order' => 'desc']]);
/** @var Pagerfanta $pager */
$pager = $finder->findPaginated($query);
$pager->setMaxPerPage(20);
$pager->setCurrentPage(max(1, (int) $request->query->get('page', 1)));
$articles = iterator_to_array($pager->getCurrentPageResults());
return $this->render('forum/tag.html.twig', [
'tag' => $tag,
'articles' => $articles,
'pager' => $pager, // expose if you want numbered pagination links
'topics' => $this->getHydratedTopics($index),
]);
}
// ---------- Helpers ----------
/**
* Flatten all tags from the taxonomy into a unique set.
* @return array<string, true>
*/
private function flattenAllTags(array $categories): array
{
$set = [];
foreach ($categories as $cat) {
foreach ($cat['subcategories'] as $sub) {
foreach ($sub['tags'] as $tag) {
$set[strtolower($tag)] = true;
}
}
}
return $set;
}
/**
* Run one ES query that returns counts for each tag (OR scope per tag).
* Uses a Filters aggregation keyed by tag to avoid N queries.
*
* @param \Elastica\Index $index
* @param string[] $tags
* @return array<string,int>
*/
private function fetchTagCounts(\Elastica\Index $index, array $tags): array
{
$tags = array_values(array_unique(array_map('strtolower', array_map('trim', $tags))));
if (!$tags) return [];
$q = new Query(new Query\MatchAll());
$filters = new FiltersAgg('tag_counts');
foreach ($tags as $tag) {
$b = new BoolQuery();
$b->addFilter(new Term(['topics' => $tag])); // topics must be keyword + lowercase normalizer
$filters->addFilter($b, $tag);
}
$q->addAggregation($filters);
$q->setSize(0);
$res = $index->search($q);
$agg = $res->getAggregation('tag_counts')['buckets'] ?? [];
$out = [];
foreach ($tags as $tag) {
$out[$tag] = isset($agg[$tag]['doc_count']) ? (int) $agg[$tag]['doc_count'] : 0;
}
return $out;
}
/**
* Rehydrate taxonomy with counts per subcategory (sum of its tags).
* @param array<string,int> $counts
*/
private function hydrateCategoryCounts(array $taxonomy, array $counts): array
{
$out = [];
foreach ($taxonomy as $catKey => $cat) {
$subs = [];
foreach ($cat['subcategories'] as $subKey => $sub) {
$sum = 0;
foreach ($sub['tags'] as $tag) {
$sum += $counts[strtolower($tag)] ?? 0;
}
$subs[$subKey] = $sub + ['count' => $sum];
}
$out[$catKey] = $cat;
$out[$catKey]['subcategories'] = $subs;
}
return $out;
}
/**
* (Optional) Fetch latest threads for a given OR-scope of tag groups.
* You can replace this with your Finder if you want entity hydration.
*
* @param array<int,array<int,string>> $tagGroups e.g. [ ['bitcoin','lightning'] ]
* @return array<int,array<string,mixed>>
*/
private function fetchThreads(\Elastica\Index $index, array $tagGroups, int $size = 200): array
{
$bool = new BoolQuery();
// For a simple OR across tags: use Terms query on 'topics'
// If you pass multiple groups and want AND across groups, adapt here.
$flatTags = [];
foreach ($tagGroups as $g) { foreach ($g as $t) { $flatTags[] = strtolower($t); } }
$flatTags = array_values(array_unique($flatTags));
if ($flatTags) {
$bool->addFilter(new Terms('topics', $flatTags));
}
$q = (new Query($bool))
->setSize($size)
->addSort(['createdAt' => ['order' => 'desc']]);
$rs = $index->search($q);
// Map raw sources you need (adjust to your mapping)
return array_map(static function (\Elastica\Result $hit) {
$s = $hit->getSource();
return [
'id' => $s['id'] ?? $hit->getId(),
'title' => $s['title'] ?? '(untitled)',
'excerpt' => $s['excerpt'] ?? null,
'topics' => $s['topics'] ?? [],
'created_at' => $s['createdAt'] ?? null,
];
}, $rs->getResults());
}
private function getHydratedTopics(\Elastica\Index $index): array
{
$allTags = $this->flattenAllTags(ForumTopics::TOPICS);
$counts = $this->fetchTagCounts($index, array_keys($allTags));
return $this->hydrateCategoryCounts(ForumTopics::TOPICS, $counts);
}
}

5
src/Controller/SearchController.php

@ -4,6 +4,7 @@ declare(strict_types=1); @@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller;
use App\Util\ForumTopics;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@ -13,6 +14,8 @@ class SearchController extends AbstractController @@ -13,6 +14,8 @@ class SearchController extends AbstractController
#[Route('/search')]
public function index(): Response
{
return $this->render('pages/search.html.twig');
return $this->render('pages/search.html.twig', [
'topics' => ForumTopics::TOPICS,
]);
}
}

12
src/Twig/Components/Atoms/ForumAside.php

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
<?php
namespace App\Twig\Components\Atoms;
use App\Util\ForumTopics;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class ForumAside
{
public array $topics = ForumTopics::TOPICS;
}

12
src/Twig/Components/Atoms/PageHeading.php

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
<?php
namespace App\Twig\Components\Atoms;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class PageHeading
{
public string $heading;
public ?string $tagline = null;
}

192
src/Util/ForumTopics.php

@ -0,0 +1,192 @@ @@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
namespace App\Util;
class ForumTopics
{
public const TOPICS = [
// ─────────── LIFESTYLE ───────────
'lifestyle' => [
'name' => 'Lifestyle',
'subcategories' => [
'travel' => [
'name' => 'Travel & Exploration',
'tags' => ['travel', 'adventure', 'destinations', 'culture', 'photography'],
],
'health' => [
'name' => 'Health & Wellness',
'tags' => ['health', 'fitness', 'wellness', 'nutrition', 'meditation', 'mental-health'],
],
'relationships' => [
'name' => 'Relationships & Family',
'tags' => ['relationships', 'family', 'parenting', 'marriage', 'friendship', 'community'],
],
'christianity' => [
'name' => 'Christianity & Faith',
'tags' => ['jesus', 'christian', 'bible', 'faith', 'spirituality', 'religion'],
],
'philosophy' => [
'name' => 'Philosophy & Ethics',
'tags' => ['philosophy', 'ethics', 'existentialism', 'metaphysics', 'logic'],
],
'education' => [
'name' => 'Education & Learning',
'tags' => ['education', 'learning', 'school', 'university', 'teaching', 'homeschool'],
],
'finance' => [
'name' => 'Personal Finance',
'tags' => ['budgeting', 'saving', 'investing', 'debt', 'retirement', 'economy'],
],
],
],
// ─────────── TECH ───────────
'tech' => [
'name' => 'Tech',
'subcategories' => [
'bitcoin' => [
'name' => 'Bitcoin & Sound Money',
'tags' => ['bitcoin', 'lightning', 'decentralization', 'freedom', 'privacy', 'sovereignty'],
],
'ai' => [
'name' => 'Artificial Intelligence',
'tags' => ['ai', 'machine-learning', 'llm', 'neural-networks', 'automation', 'robotics'],
],
'nostr' => [
'name' => 'Nostr & Decentralized Social',
'tags' => ['nostr', 'fediverse', 'social', 'protocol', 'identity', 'nip'],
],
'software' => [
'name' => 'Software Development',
'tags' => ['code', 'programming', 'development', 'open-source', 'python', 'php', 'javascript'],
],
'hardware' => [
'name' => 'Hardware & Gadgets',
'tags' => ['hardware', 'devices', 'gadgets', 'controllers', 'iot', 'electronics'],
],
'cybersecurity' => [
'name' => 'Cybersecurity & Privacy',
'tags' => ['security', 'privacy', 'encryption', 'hacking', 'infosec', 'vpn'],
],
'science' => [
'name' => 'Science & Innovation',
'tags' => ['science', 'innovation', 'research', 'biology', 'physics', 'space', 'technology'],
],
],
],
// ─────────── ART & CULTURE ───────────
'art' => [
'name' => 'Art & Culture',
'subcategories' => [
'photography' => [
'name' => 'Photography',
'tags' => ['photography', 'photojournalism', 'portrait', 'street', 'nature'],
],
'music' => [
'name' => 'Music',
'tags' => ['music', 'audio', 'sound', 'composition', 'performance', 'production'],
],
'writing' => [
'name' => 'Writing & Literature',
'tags' => ['writing', 'literature', 'books', 'poetry', 'fiction', 'non-fiction'],
],
'film' => [
'name' => 'Film & Video',
'tags' => ['film', 'video', 'cinema', 'documentary', 'animation', 'production'],
],
'design' => [
'name' => 'Design & Creativity',
'tags' => ['design', 'art', 'creativity', 'ui', 'ux', 'graphic-design'],
],
'history' => [
'name' => 'History & Society',
'tags' => ['history', 'society', 'politics', 'culture', 'anthropology', 'archaeology'],
],
],
],
// ─────────── BUSINESS ───────────
'business' => [
'name' => 'Business',
'subcategories' => [
'entrepreneurship' => [
'name' => 'Entrepreneurship',
'tags' => ['entrepreneurship', 'startup', 'business', 'innovation', 'leadership'],
],
'marketing' => [
'name' => 'Marketing & Sales',
'tags' => ['marketing', 'sales', 'advertising', 'branding', 'customer', 'growth'],
],
'economics' => [
'name' => 'Economics & Finance',
'tags' => ['economics', 'finance', 'markets', 'trading', 'policy', 'macro'],
],
'management' => [
'name' => 'Management & Strategy',
'tags' => ['management', 'strategy', 'operations', 'productivity', 'leadership'],
],
'real-estate' => [
'name' => 'Real Estate',
'tags' => ['real-estate', 'property', 'housing', 'investment', 'development'],
],
],
],
// ─────────── SPORTS ───────────
'sports' => [
'name' => 'Sports',
'subcategories' => [
'fitness' => [
'name' => 'Fitness & Training',
'tags' => ['fitness', 'training', 'exercise', 'health', 'athletics', 'performance'],
],
'outdoor' => [
'name' => 'Outdoor Activities',
'tags' => ['outdoor', 'hiking', 'camping', 'climbing', 'adventure', 'nature'],
],
'team-sports' => [
'name' => 'Team Sports',
'tags' => ['football', 'basketball', 'baseball', 'soccer', 'hockey', 'team'],
],
'combat' => [
'name' => 'Combat Sports',
'tags' => ['mma', 'boxing', 'wrestling', 'martial-arts', 'combat', 'fighting'],
],
'esports' => [
'name' => 'Esports & Gaming',
'tags' => ['esports', 'gaming', 'video-games', 'competition', 'streaming'],
],
],
],
// ─────────── NEWS & POLITICS ───────────
'news' => [
'name' => 'News & Politics',
'subcategories' => [
'politics' => [
'name' => 'Politics & Government',
'tags' => ['politics', 'government', 'policy', 'election', 'democracy', 'law'],
],
'world-news' => [
'name' => 'World News',
'tags' => ['news', 'world', 'international', 'geopolitics', 'diplomacy'],
],
'us-news' => [
'name' => 'US News',
'tags' => ['us', 'america', 'united-states', 'domestic', 'national'],
],
'activism' => [
'name' => 'Activism & Social Issues',
'tags' => ['activism', 'social', 'justice', 'equality', 'rights', 'protest'],
],
'media' => [
'name' => 'Media & Journalism',
'tags' => ['media', 'journalism', 'press', 'reporting', 'freedom', 'censorship'],
],
],
],
];
}

13
templates/components/Atoms/ForumAside.html.twig

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
<div class="d-flex gap-3 center mt-3 mb-3 ln-section--reader">
<h2 class="mb-4">Forum</h2>
</div>
{% for catKey, category in topics %}
<details class="mb-2">
<summary>{{ category.name }}</summary>
<ul class="list-unstyled ms-2">
{% for subKey, sub in category.subcategories %}
<li class="mt-2"><a href="{{ path('forum_topic', {'key': catKey ~ '-' ~ subKey}) }}">{{ sub.name }}</a></li>
{% endfor %}
</ul>
</details>
{% endfor %}

8
templates/components/Atoms/PageHeading.html.twig

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
<section {{ attributes }} class="d-flex gap-3 center ln-section--latest-articles">
<div class="container mt-5 mb-5">
<h1>{{ heading }}</h1>
{% if tagline %}
<p class="eyebrow">{{ tagline }}</p>
{% endif %}
</div>
</section>

32
templates/forum/index.html.twig

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
{% extends 'layout.html.twig' %}
{% block body %}
<twig:Atoms:PageHeading heading="Forum" tagline="Sorted by topic"/>
{% for catKey, category in topics %}
<section class="d-flex gap-3 center ln-section--newsstand">
<div class="container mt-5 mb-5">
<h2>{{ category.name }}</h2>
</div>
</section>
<div class="subcategories-grid">
{% for subKey, sub in category.subcategories %}
<div class="sub-card">
<h3><a href="{{ path('forum_topic', {'key': catKey ~ '-' ~ subKey}) }}">{{ sub.name }}</a></h3>
<div class="d-flex flex-row">
<div class="tags m-0">
{% for tag in sub.tags %}
<a class="tag" href="{{ path('forum_tag', {'tag': tag}) }}">{{ tag }}</a>
{% endfor %}
</div>
<div class="count">{{ sub.count|default(0) }}</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
{% endblock %}
{% block aside %}
<twig:Atoms:ForumAside />
{% endblock %}

22
templates/forum/tag.html.twig

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
{% extends 'layout.html.twig' %}
{% block body %}
<twig:Atoms:PageHeading heading="{{ tag|capitalize }}" />
<div class="articles-list">
{% for article in articles %}
<div class="article-item">
<h3><a href="{{ path('article-slug', {slug: article.slug|url_encode}) }}">{{ article.title }}</a></h3>
<p>{{ article.summary|slice(0, 200) }}{% if article.summary|length > 200 %}...{% endif %}</p>
<small>Published: {{ article.createdAt|date('Y-m-d H:i') }}</small>
</div>
{% endfor %}
</div>
{% if articles is empty %}
<p>No articles found for this tag.</p>
{% endif %}
{% endblock %}
{% block aside %}
<twig:Atoms:ForumAside />
{% endblock %}

22
templates/forum/topic.html.twig

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
{% extends 'layout.html.twig' %}
{% block body %}
<twig:Atoms:PageHeading heading="{{ topic.name }}" tagline="{{ tags|keys|join(', ') }}"/>
<div class="articles-list">
{% for article in articles %}
<div class="article-item">
<h3><a href="{{ path('article-slug', {slug: article.slug|url_encode}) }}">{{ article.title }}</a></h3>
<p>{{ article.summary|slice(0, 200) }}{% if article.summary|length > 200 %}...{% endif %}</p>
<small>Published: {{ article.createdAt|date('Y-m-d H:i') }}</small>
</div>
{% endfor %}
</div>
{% if articles is empty %}
<p>No articles found for this tag.</p>
{% endif %}
{% endblock %}
{% block aside %}
<twig:Atoms:ForumAside />
{% endblock %}

7
templates/pages/latest-articles.html.twig

@ -1,12 +1,7 @@ @@ -1,12 +1,7 @@
{% extends 'layout.html.twig' %}
{% block body %}
<section class="d-flex gap-3 center ln-section--latest-articles">
<div class="container mt-5 mb-5">
<h1>Latest Articles</h1>
<p class="eyebrow">Fresh off the presses</p>
</div>
</section>
<twig:Atoms:PageHeading heading="Latest Articles" tagline="Fresh off the presses"/>
<section class="w-container mb-5">
{% if articles is empty %}

6
templates/pages/media-discovery.html.twig

@ -6,11 +6,7 @@ @@ -6,11 +6,7 @@
{% endblock %}
{% block body %}
<div class="discover-header">
<h1>Multimedia</h1>
<p class="discover-subtitle">Discovery through serendipity</p>
</div>
<twig:Atoms:PageHeading heading="Multimedia" tagline="Discovery through serendipity"/>
<div class="w-container">
{% if error is defined %}

4
templates/pages/search.html.twig

@ -12,3 +12,7 @@ @@ -12,3 +12,7 @@
</section>
<twig:SearchComponent />
{% endblock %}
{% block aside %}
<twig:Atoms:ForumAside />
{% endblock %}

Loading…
Cancel
Save