unfinished forum stuff

This commit is contained in:
flash 2018-05-18 03:20:27 +02:00
parent e59ea5dfb7
commit 387ab2ccb4
19 changed files with 573 additions and 20 deletions

View file

@ -5,6 +5,10 @@
margin: 0; margin: 0;
} }
&__none {
padding: 2px 5px;
}
&__entry { &__entry {
display: flex; display: flex;
padding: 2px 0; padding: 2px 0;
@ -16,8 +20,6 @@
&__icon { &__icon {
width: 50px; width: 50px;
line-height: 50px;
font-size: 2.5em;
text-align: center; text-align: center;
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
@ -26,6 +28,7 @@
&__info { &__info {
flex-grow: 1; flex-grow: 1;
flex-shrink: 1; flex-shrink: 1;
padding-left: 6px;
} }
&__title { &__title {

View file

@ -0,0 +1,3 @@
.forum__topics {
//
}

View file

@ -17,6 +17,16 @@
flex-direction: column; flex-direction: column;
} }
&--left {
justify-content: left;
padding-left: 25px;
}
&--right {
justify-content: right;
padding-right: 25px;
}
&--top { &--top {
border-top-width: 0; border-top-width: 0;
border-bottom-width: 1px; border-bottom-width: 1px;

View file

@ -2,7 +2,6 @@
namespace Misuzu\DatabaseMigrations\InitialStructure; namespace Misuzu\DatabaseMigrations\InitialStructure;
use PDO; use PDO;
use Misuzu\Database;
function migrate_up(PDO $conn): void function migrate_up(PDO $conn): void
{ {

View file

@ -0,0 +1,135 @@
<?php
namespace Misuzu\DatabaseMigrations\ForumStructure;
use PDO;
function migrate_up(PDO $conn): void
{
$conn->exec("
CREATE TABLE `msz_forum_categories` (
`forum_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`forum_order` INT(10) UNSIGNED NOT NULL DEFAULT '1',
`forum_parent` INT(10) UNSIGNED NOT NULL DEFAULT '0',
`forum_name` VARCHAR(255) NOT NULL,
`forum_type` TINYINT(4) NOT NULL DEFAULT '0',
`forum_description` TEXT NULL,
`forum_link` VARCHAR(255) NULL DEFAULT NULL,
`forum_created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`forum_archived` TINYINT(1) NOT NULL DEFAULT '0',
`forum_hidden` TINYINT(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`forum_id`),
INDEX `forums_indices` (`forum_order`, `forum_parent`, `forum_type`)
)
");
$conn->exec("
CREATE TABLE `msz_forum_topics` (
`topic_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`forum_id` INT(10) UNSIGNED NOT NULL,
`user_id` INT(10) UNSIGNED NULL DEFAULT NULL,
`topic_title` VARCHAR(255) NOT NULL,
`topic_created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`topic_bumped` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`topic_deleted` TIMESTAMP NULL DEFAULT NULL,
`topic_view_count` INT(10) NOT NULL DEFAULT '0',
`topic_type` TINYINT(4) NOT NULL DEFAULT '0',
`topic_status` TINYINT(4) NOT NULL DEFAULT '0',
PRIMARY KEY (`topic_id`),
INDEX `topics_forum_id_foreign` (`forum_id`),
INDEX `topics_user_id_foreign` (`user_id`),
INDEX `topics_indices` (`topic_bumped`, `topic_type`),
CONSTRAINT `topics_forum_id_foreign`
FOREIGN KEY (`forum_id`)
REFERENCES `msz_forum_categories` (`forum_id`)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT `topics_user_id_foreign`
FOREIGN KEY (`user_id`)
REFERENCES `msz_users` (`user_id`)
ON UPDATE CASCADE
ON DELETE CASCADE
)
");
$conn->exec("
CREATE TABLE `msz_forum_posts` (
`post_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`topic_id` INT(10) UNSIGNED NOT NULL,
`forum_id` INT(10) UNSIGNED NOT NULL,
`user_id` INT(10) UNSIGNED NULL DEFAULT NULL,
`post_title` VARCHAR(255) NOT NULL,
`post_ip` BLOB NOT NULL,
`post_created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`post_edited` TIMESTAMP NULL DEFAULT NULL,
`post_deleted` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`post_id`),
INDEX `posts_topic_id_foreign` (`topic_id`),
INDEX `posts_forum_id_foreign` (`forum_id`),
INDEX `posts_user_id_foreign` (`user_id`),
INDEX `posts_indices` (`post_created`),
CONSTRAINT `posts_topic_id_foreign`
FOREIGN KEY (`topic_id`)
REFERENCES `msz_forum_topics` (`topic_id`)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT `posts_forum_id_foreign`
FOREIGN KEY (`forum_id`)
REFERENCES `msz_forum_categories` (`forum_id`)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT `posts_user_id_foreign`
FOREIGN KEY (`user_id`)
REFERENCES `msz_users` (`user_id`)
ON UPDATE CASCADE
ON DELETE SET NULL
)
");
$conn->exec("
CREATE TABLE `msz_forum_categories_track` (
`user_id` INT(10) UNSIGNED NOT NULL,
`forum_id` INT(10) UNSIGNED NOT NULL,
`track_last_read` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `categories_track_forum_id_foreign` (`forum_id`),
INDEX `categories_track_user_id_foreign` (`user_id`),
CONSTRAINT `categories_track_forum_id_foreign`
FOREIGN KEY (`forum_id`)
REFERENCES `msz_forum_categories` (`forum_id`)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT `categories_track_user_id_foreign`
FOREIGN KEY (`user_id`)
REFERENCES `msz_users` (`user_id`)
ON UPDATE CASCADE
ON DELETE CASCADE
)
");
$conn->exec("
CREATE TABLE `msz_forum_topics_track` (
`user_id` INT(10) UNSIGNED NOT NULL,
`topic_id` INT(10) UNSIGNED NOT NULL,
`track_last_read` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `topics_track_topic_id_foreign` (`topic_id`),
INDEX `topics_track_user_id_foreign` (`user_id`),
CONSTRAINT `topics_track_topic_id_foreign`
FOREIGN KEY (`topic_id`)
REFERENCES `msz_forum_topics` (`topic_id`)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT `topics_track_user_id_foreign`
FOREIGN KEY (`user_id`)
REFERENCES `msz_users` (`user_id`)
ON UPDATE CASCADE
ON DELETE CASCADE
)
");
}
function migrate_down(PDO $conn): void
{
$conn->exec('DROP TABLE `msz_forum_topics_track`');
$conn->exec('DROP TABLE `msz_forum_posts`');
$conn->exec('DROP TABLE `msz_forum_topics`');
$conn->exec('DROP TABLE `msz_forum_categories`');
}

130
public/forum/forum.php Normal file
View file

@ -0,0 +1,130 @@
<?php
use Misuzu\Database;
require_once __DIR__ . '/../../misuzu.php';
$forumId = (int)($_GET['f'] ?? 0);
if ($forumId === 0) {
header('Location: /forum/');
exit;
}
$db = Database::connection();
$templating = $app->getTemplating();
if ($forumId > 0) {
$getForum = $db->prepare('
SELECT
`forum_id`, `forum_name`, `forum_type`, `forum_link`, `forum_parent`
FROM `msz_forum_categories`
WHERE `forum_id` = :forum_id
');
$getForum->bindValue('forum_id', $forumId);
$forum = $getForum->execute() ? $getForum->fetch() : [];
}
if (empty($forum) || ($forum['forum_type'] == 2 && empty($forum['forum_link']))) {
http_response_code(404);
echo $templating->render('errors.404');
return;
}
if ($forum['forum_type'] == 2) {
header('Location: ' . $forum['forum_link']);
return;
}
// declare this, templating engine assumes it exists
$topics = [];
// no need to fetch topics for categories (or links but we're already done with those at this point)
if ($forum['forum_type'] == 0) {
$getTopics = $db->prepare('
SELECT
t.`topic_id`, t.`topic_title`, t.`topic_view_count`,
au.`user_id` as `author_id`, au.`username` as `author_name`,
COUNT(p.`post_id`) as `topic_post_count`,
MIN(p.`post_id`) as `topic_first_post_id`,
MAX(p.`post_id`) as `topic_last_post_id`,
COALESCE(ar.`role_colour`, CAST(0x40000000 AS UNSIGNED)) as `author_colour`
FROM `msz_forum_topics` as t
LEFT JOIN `msz_users` as au
ON t.`user_id` = au.`user_id`
LEFT JOIN `msz_roles` as ar
ON ar.`role_id` = au.`display_role`
LEFT JOIN `msz_forum_posts` as p
ON t.`topic_id` = p.`topic_id`
WHERE t.`forum_id` = :forum_id
AND t.`topic_deleted` IS NULL
GROUP BY t.`topic_id`
ORDER BY t.`topic_type`, t.`topic_bumped`
');
$getTopics->bindValue('forum_id', $forum['forum_id']);
$topics = $getTopics->execute() ? $getTopics->fetchAll() : $topics;
}
$getSubforums = $db->prepare('
SELECT
`forum_id`, `forum_name`, `forum_description`, `forum_type`, `forum_link`,
(
SELECT COUNT(t.`topic_id`)
FROM `msz_forum_topics` as t
WHERE t.`forum_id` = f.`forum_id`
) as `forum_topic_count`,
(
SELECT COUNT(p.`post_id`)
FROM `msz_forum_posts` as p
WHERE p.`forum_id` = f.`forum_id`
) as `forum_post_count`
FROM `msz_forum_categories` as f
WHERE `forum_parent` = :forum_id
AND `forum_hidden` = false
');
$getSubforums->bindValue('forum_id', $forum['forum_id']);
$forum['forum_subforums'] = $getSubforums->execute() ? $getSubforums->fetchAll() : [];
if (count($forum['forum_subforums']) > 0) {
// this really, really needs a better name
$getSubSubs = $db->prepare('
SELECT `forum_id`, `forum_name`
FROM `msz_forum_categories`
WHERE `forum_parent` = :forum_id
AND `forum_hidden` = false
');
foreach ($forum['forum_subforums'] as $skey => $subforum) {
$getSubSubs->bindValue('forum_id', $subforum['forum_id']);
$forum['forum_subforums'][$skey]['forum_subforums'] = $getSubSubs->execute() ? $getSubSubs->fetchAll() : [];
}
}
$lastParent = $forum['forum_parent'];
$breadcrumbs = [$forum['forum_name'] => '/forum/forum.php?f=' . $forum['forum_id']];
$getBreadcrumb = $db->prepare('
SELECT `forum_id`, `forum_name`, `forum_parent`
FROM `msz_forum_categories`
WHERE `forum_id` = :forum_id
');
while ($lastParent > 0) {
$getBreadcrumb->bindValue('forum_id', $lastParent);
if (!$getBreadcrumb->execute()) {
break;
}
$parentForum = $getBreadcrumb->fetch();
$breadcrumbs[$parentForum['forum_name']] = '/forum/forum.php?f=' . $parentForum['forum_id'];
$lastParent = $parentForum['forum_parent'];
}
$breadcrumbs['Forums'] = '/forum/';
$breadcrumbs = array_reverse($breadcrumbs);
echo $app->getTemplating()->render('forum.forum', [
'forum_info' => $forum,
'forum_breadcrumbs' => $breadcrumbs,
'forum_topics' => $topics,
]);

73
public/forum/index.php Normal file
View file

@ -0,0 +1,73 @@
<?php
use Misuzu\Database;
require_once __DIR__ . '/../../misuzu.php';
$categories = $db->query('
SELECT
f.`forum_id`, f.`forum_name`, f.`forum_type`,
(
SELECT COUNT(`forum_id`)
FROM `msz_forum_categories` as sf
WHERE sf.`forum_parent` = f.`forum_id`
) as `forum_children`
FROM `msz_forum_categories` as f
WHERE f.`forum_parent` = 0
AND f.`forum_type` = 1
AND f.`forum_hidden` = false
GROUP BY f.`forum_id`
ORDER BY f.`forum_order`
')->fetchAll();
$categories = array_merge([
[
'forum_id' => 0,
'forum_name' => 'Forums',
'forum_children' => 0,
'forum_type' => 1,
],
], $categories);
$getSubCategories = $db->prepare('
SELECT
f.`forum_id`, f.`forum_name`, f.`forum_description`, f.`forum_type`, f.`forum_link`,
(
SELECT COUNT(t.`topic_id`)
FROM `msz_forum_topics` as t
WHERE t.`forum_id` = f.`forum_id`
) as `forum_topic_count`,
(
SELECT COUNT(p.`post_id`)
FROM `msz_forum_posts` as p
WHERE p.`forum_id` = f.`forum_id`
) as `forum_post_count`
FROM `msz_forum_categories` as f
WHERE f.`forum_parent` = :forum_id
AND f.`forum_hidden` = false
AND ((f.`forum_parent` = 0 AND f.`forum_type` != 1) OR f.`forum_parent` != 0)
ORDER BY f.`forum_order`
');
foreach ($categories as $key => $category) {
// replace these magic numbers with a constant later, only categories and discussion forums may have subs
if (!in_array($category['forum_type'], [0, 1])
&& ($category['forum_id'] === 0 || $category['forum_children'] > 0)) {
continue;
}
$getSubCategories->bindValue('forum_id', $category['forum_id']);
$categories[$key]['forum_subforums'] = $getSubCategories->execute() ? $getSubCategories->fetchAll() : [];
// one level down more!
foreach ($categories[$key]['forum_subforums'] as $skey => $sub) {
$getSubCategories->bindValue('forum_id', $sub['forum_id']);
$categories[$key]['forum_subforums'][$skey]['forum_subforums']
= $getSubCategories->execute() ? $getSubCategories->fetchAll() : [];
}
}
$categories[0]['forum_children'] = count($categories[0]['forum_subforums']);
echo $app->getTemplating()->render('forum.index', [
'forum_categories' => $categories,
]);

6
public/forum/topic.php Normal file
View file

@ -0,0 +1,6 @@
<?php
use Misuzu\Database;
require_once __DIR__ . '/../../misuzu.php';
echo $app->getTemplating()->render('forum.topic');

View file

@ -1,6 +1,5 @@
<?php <?php
use Misuzu\Database; use Misuzu\Database;
use Misuzu\DatabaseMigrationManager;
require_once __DIR__ . '/../misuzu.php'; require_once __DIR__ . '/../misuzu.php';

View file

@ -46,7 +46,17 @@ switch ($mode) {
SELECT SELECT
u.*, u.*,
r.`role_title` as `user_title`, r.`role_title` as `user_title`,
COALESCE(r.`role_colour`, CAST(0x40000000 AS UNSIGNED)) as `display_colour` COALESCE(r.`role_colour`, CAST(0x40000000 AS UNSIGNED)) as `display_colour`,
(
SELECT COUNT(`topic_id`)
FROM `msz_forum_topics` as t
WHERE t.`user_id` = u.`user_id`
) as `forum_topic_count`,
(
SELECT COUNT(`post_id`)
FROM `msz_forum_posts` as p
WHERE p.`user_id` = u.`user_id`
) as `forum_post_count`
FROM `msz_users` as u FROM `msz_users` as u
LEFT JOIN `msz_roles` as r LEFT JOIN `msz_roles` as r
ON r.`role_id` = u.`display_role` ON r.`role_id` = u.`display_role`

View file

@ -3,7 +3,7 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<a href="?m=create" class="button">Create new Role</a> <a href="?v=role" class="button">Create new Role</a>
</div> </div>
<div class="container listing role-listing"> <div class="container listing role-listing">

View file

@ -0,0 +1,20 @@
{% extends '@mio/forum/master.twig' %}
{% from '@mio/macros.twig' import navigation %}
{% from '@mio/forum/macros.twig' import forum_category_listing, forum_topic_listing %}
{% set title = forum_info.forum_name %}
{% set canonical_url = '/forum/forum.php?f=' ~ forum_info.forum_id %}
{% block content %}
{{ navigation(forum_breadcrumbs, forum_breadcrumbs|last, true, null, 'left') }}
{% if forum_info.forum_subforums|length > 0 or forum_info.forum_type == 1 %}
{{ forum_category_listing(forum_info.forum_subforums, 'Forums') }}
{% endif %}
{% if forum_info.forum_type == 0 %}
{{ forum_topic_listing(forum_topics) }}
{% endif %}
{{ navigation(mio_navigation, '/forum/') }}
{% endblock %}

View file

@ -0,0 +1,27 @@
{% extends '@mio/forum/master.twig' %}
{% from '@mio/macros.twig' import navigation %}
{% from '@mio/forum/macros.twig' import forum_category_listing %}
{% set title = 'Forum Listing' %}
{% set canonical_url = '/forum/' %}
{% block content %}
{% for category in forum_categories %}
{% if category.forum_children > 0 %}
{{ forum_category_listing(category.forum_subforums, category.forum_name) }}
{% endif %}
{% endfor %}
<div class="container">
<div class="container__title">Actions</div>
<div class="container__content">
<button class="input__button">Mark All Read</button>
<button class="input__button">Unanswered Posts</button>
<button class="input__button">New Posts</button>
<button class="input__button">Your Posts</button>
</div>
</div>
{{ navigation(mio_navigation, '/forum/') }}
{% endblock %}

115
views/mio/forum/macros.twig Normal file
View file

@ -0,0 +1,115 @@
{% macro forum_category_listing(forums, title) %}
{% from _self import forum_category_entry %}
<div class="container forum__listing">
<div class="container__title">{{ title }}</div>
<div class="container__content forum__listing__forums">
{% if forums|length > 0 %}
{% for forum in forums %}
{{ forum_category_entry(forum) }}
{% endfor %}
{% else %}
<div class="forum__listing__none">
This category is empty.
</div>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro forum_category_entry(forum, forum_icon) %}
{% set forum_icon = forum_icon|default(null) %}
{% if forum_icon is null %}
{% if forum.forum_archived is defined and forum.forum_archived %}
{% set forum_icon = 'https://static.flash.moe/images/forum-icons/default-archived-%s.png' %}
{% elseif forum.forum_type is defined %}
{% if forum.forum_type == 2 %}
{% set forum_icon = 'https://static.flash.moe/images/forum-icons/default-link-%s.png' %}
{% elseif forum.forum_type == 1 %}
{% set forum_icon = 'https://static.flash.moe/images/forum-icons/default-category-%s.png' %}
{% endif %}
{% endif %}
{% set forum_icon = forum_icon|default('https://static.flash.moe/images/forum-icons/default-forum-%s.png') %}
{% endif %}
<div class="forum__listing__entry">
<div class="forum__listing__entry__icon">
<img src="{{ forum_icon|format('read') }}" alt="read">
</div>
<div class="forum__listing__entry__info">
<a href="/forum/forum.php?f={{ forum.forum_id }}" class="forum__listing__entry__title">{{ forum.forum_name }}</a>
<div class="forum__listing__entry__description">
{{ forum.forum_description|nl2br }}
{% if forum.forum_subforums is defined and forum.forum_subforums|length > 0 %}
<br>
{% set listing = [] %}
{% for subforum in forum.forum_subforums %}
{% set listing = listing|merge(['<a href="/forum/forum.php?f='|raw ~ subforum.forum_id ~ '">'|raw ~ subforum.forum_name ~ '</a>'|raw]) %}
{% endfor %}
{{ listing|join(', ')|raw }}
{% endif %}
</div>
</div>
<div class="forum__listing__entry__stats">
<div class="forum__listing__entry__topics">{{ forum.forum_topic_count|number_format }}</div>
<div class="forum__listing__entry__posts">{{ forum.forum_post_count|number_format }}</div>
</div>
<div class="forum__listing__entry__activity">
<div class="forum__listing__entry__activity__none">
There are no posts in this forum yet.
</div>
</div>
</div>
{% endmacro %}
{% macro forum_topic_listing(topics) %}
{% from _self import forum_topic_entry %}
<div class="container forum__topics">
<div class="container__title">Topics</div>
<div class="container__content forum__topics__listing">
{% if topics|length > 0 %}
{% for topic in topics %}
{{ forum_topic_entry(topic) }}
{% endfor %}
{% else %}
<div class="forum__topics__none">
There are no topics in this forum.
</div>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro forum_topic_entry(topic) %}
<div class="forum__topics__entry">
<div class="forum__topics__icon">
eek
</div>
<div class="forum__topics__info">
<div class="forum__topics__info__title">
<a href="/forum/topic.php?t={{ topic.topic_id }}">{{ topic.topic_title }}</a>
</div>
{% if topic.author_id is not null %}
<div class="forum__topics__info__author">
by <a href="/profile.php?u={{ topic.author_id }}" style="color:{{ topic.author_colour|colour_get_css }}">{{ topic.author_name }}</a>
</div>
{% endif %}
</div>
<div class="forum__topics__stats">
<div class="forum__topics__stat forum__topics__stat--posts">{{ topic.topic_post_count }}</div>
<div class="forum__topics__stat forum__topics__stat--views">{{ topic.topic_view_count }}</div>
</div>
<div class="forum__topics__last-reply">
last post data required here, display "no replies" when only one post is present
</div>
</div>
{% endmacro %}

View file

@ -0,0 +1 @@
{% extends '@mio/master.twig' %}

View file

View file

@ -4,12 +4,13 @@
{% endspaceless %} {% endspaceless %}
{% endmacro %} {% endmacro %}
{% macro navigation(links, current, top, fmt) %} {% macro navigation(links, current, top, fmt, align) %}
{% set top = top|default(false) == true %} {% set top = top|default(false) == true %}
{% set align = align|default('centre') %}
{% set current = current|default(null) %} {% set current = current|default(null) %}
{% set fmt = fmt|default('%s') %} {% set fmt = fmt|default('%s') %}
<ul class="navigation{% if top %} navigation--top{% endif %}"> <ul class="navigation{% if top %} navigation--top{% endif %}{% if align != 'centre' %} navigation--{{ align }}{% endif %}">
{% for name, url in links %} {% for name, url in links %}
<li class="navigation__option{% if url == current or name == current %} navigation__option--selected{% endif %}"><a href="{{ fmt|format(url) }}" class="navigation__link">{{ name }}</a></li> <li class="navigation__option{% if url == current or name == current %} navigation__option--selected{% endif %}"><a href="{{ fmt|format(url) }}" class="navigation__link">{{ name }}</a></li>
{% endfor %} {% endfor %}

View file

@ -3,6 +3,7 @@
{% set mio_navigation = { {% set mio_navigation = {
'Home': '/', 'Home': '/',
'News': '/news.php', 'News': '/news.php',
'Forum': '/forum/',
'Chat': 'https://chat.flashii.net', 'Chat': 'https://chat.flashii.net',
} %} } %}

View file

@ -100,7 +100,7 @@
</div> </div>
</div> </div>
{# if profile.last_seen.timestamp > 0 #} {% if profile.last_seen is not null %}
<div class="profile__info__row" title="{{ profile.last_seen }}"> <div class="profile__info__row" title="{{ profile.last_seen }}">
<div class="profile__info__column profile__info__column--heading"> <div class="profile__info__column profile__info__column--heading">
Last Seen Last Seen
@ -114,7 +114,27 @@
{{ profile.last_seen }} {{ profile.last_seen }}
</div> </div>
</div> </div>
{# endif #} {% endif %}
</div>
<div class="profile__info__block">
<div class="profile__info__row">
<div class="profile__info__column profile__info__column--heading">
Topics
</div>
<div class="profile__info__column profile__info__column--numeric">
{{ profile.forum_topic_count }}
</div>
</div>
<div class="profile__info__row">
<div class="profile__info__column profile__info__column--heading">
Posts
</div>
<div class="profile__info__column profile__info__column--numeric">
{{ profile.forum_post_count }}
</div>
</div>
</div> </div>
</div> </div>