- board-search.php can now act as an include as well as a direct request.

- boards.php completely rewritten to work with new board-search.php. Functions (sort-of) without JavaScript.
- inc/functions.php fetchBoardActivity() now pulls total board counts with a more reliable and faster method.
- inc/functions.php fetchBoardActivity() & fetchBoardTags() now expect db prefixes.
- stylesheets/stylesheet.css now handles title search bar.
- boards-tags.html completely thrown out for flat tag list styling.
- boards-search.html added for the new page layout.
- boards-table.html added for the <tbody> contents.


Signed-off-by: 8n-tech <8n-tech@users.noreply.github.com>
This commit is contained in:
8n-tech 2015-04-14 01:40:45 +10:00
parent 0ceb814ab3
commit 6d1eb9961d
7 changed files with 243 additions and 264 deletions

View File

@ -1,6 +1,11 @@
<?php <?php
include "inc/functions.php"; // We want to return a value if we're included.
// Otherwise, we will be printing a JSON object-array.
$Included = defined("TINYBOARD");
if (!$Included) {
include "inc/functions.php";
}
$CanViewUnindexed = isset($mod["type"]) && $mod["type"] <= GlobalVolunteer; $CanViewUnindexed = isset($mod["type"]) && $mod["type"] <= GlobalVolunteer;
@ -18,9 +23,10 @@ $languages = array(
/* Determine search parameters from $_GET */ /* Determine search parameters from $_GET */
$search = array( $search = array(
'lang' => false, 'lang' => false,
'nsfw' => true, 'nsfw' => true,
'tags' => false, 'tags' => false,
'titles' => false,
); );
// Include NSFW boards? // Include NSFW boards?
@ -96,67 +102,83 @@ foreach ($response['boards'] as $boardUri => &$board) {
/* Activity Fetching */ /* Activity Fetching */
$boardActivity = fetchBoardActivity( array_keys( $response['boards'] ) ); $boardActivity = fetchBoardActivity( array_keys( $response['boards'] ) );
$response['tags'] = array();
// Loop through each board and record activity to it. // Loop through each board and record activity to it.
// We will also be weighing and building a tag list. // We will also be weighing and building a tag list.
foreach ($response['boards'] as $boardUri => &$board) { foreach ($response['boards'] as $boardUri => &$board) {
$board['active'] = (int) $boardActivity['active'][ $boardUri ]; $board['active'] = (int) $boardActivity['active'][ $boardUri ];
$board['pph'] = (int) $boardActivity['average'][ $boardUri ]; $board['posts'] = (int) $boardActivity['posts'][ $boardUri ];
$board['pph'] = (int) $boardActivity['average'][ $boardUri ];
if (isset($board['tags']) && count($board['tags']) > 0) { if (isset($board['tags']) && count($board['tags']) > 0) {
foreach ($board['tags'] as $tag) { foreach ($board['tags'] as $tag) {
if (isset($response['tag'][$tag])) { if (isset($response['tags'][$tag])) {
$response['tag'][$tag] += $board['active']; $response['tags'][$tag] += $board['active'];
} }
else { else {
$response['tag'][$tag] = $board['active']; $response['tags'][$tag] = $board['active'];
} }
} }
} }
} }
// Sort boards by their popularity, then by their total posts.
$boardActivityValues = array();
foreach ($response['boards'] as $boardUri => $board) {
$boardActivityValues[$boardUri] = "{$board['active']}.{$board['posts']}";
}
array_multisort($boardActivityValues, SORT_DESC, $response['boards']);
// Get the top most popular tags. // Get the top most popular tags.
if (count($response['tag']) > 0) { if (count($response['tags']) > 0) {
// Sort by most active tags. // Sort by most active tags.
arsort( $response['tag'] ); arsort( $response['tags'] );
// Get the first n most active tags. // Get the first n most active tags.
$response['tag'] = array_splice( $response['tag'], 0, 200 ); $response['tags'] = array_splice( $response['tags'], 0, 200 );
$tagLightest = end( array_keys( $response['tag'] ) ); // $tagLightest = end( array_keys( $response['tag'] ) );
} }
/* (Please) Respond */ /* (Please) Respond */
$json = json_encode( $response ); if (!$Included) {
$json = json_encode( $response );
// Error Handling // Error Handling
switch (json_last_error()) { switch (json_last_error()) {
case JSON_ERROR_NONE: case JSON_ERROR_NONE:
$jsonError = false; $jsonError = false;
break; break;
case JSON_ERROR_DEPTH: case JSON_ERROR_DEPTH:
$jsonError = 'Maximum stack depth exceeded'; $jsonError = 'Maximum stack depth exceeded';
break; break;
case JSON_ERROR_STATE_MISMATCH: case JSON_ERROR_STATE_MISMATCH:
$jsonError = 'Underflow or the modes mismatch'; $jsonError = 'Underflow or the modes mismatch';
break; break;
case JSON_ERROR_CTRL_CHAR: case JSON_ERROR_CTRL_CHAR:
$jsonError = 'Unexpected control character found'; $jsonError = 'Unexpected control character found';
break; break;
case JSON_ERROR_SYNTAX: case JSON_ERROR_SYNTAX:
$jsonError = 'Syntax error, malformed JSON'; $jsonError = 'Syntax error, malformed JSON';
break; break;
case JSON_ERROR_UTF8: case JSON_ERROR_UTF8:
$jsonError = 'Malformed UTF-8 characters, possibly incorrectly encoded'; $jsonError = 'Malformed UTF-8 characters, possibly incorrectly encoded';
break; break;
default: default:
$jsonError = 'Unknown error'; $jsonError = 'Unknown error';
break; break;
}
if ($jsonError) {
$json = "{\"error\":\"{$jsonError}\"}";
}
// Successful output
echo $json;
} }
else {
if ($jsonError) { return $response;
$json = "{\"error\":\"{$jsonError}\"}"; }
}
// Successful output
echo $json;

View File

@ -1,6 +1,6 @@
<?php <?php
include "inc/functions.php"; include "inc/functions.php"; // October 23, 2013
include "inc/countries.php"; include "inc/countries.php";
$admin = isset($mod["type"]) && $mod["type"]<=30; $admin = isset($mod["type"]) && $mod["type"]<=30;
@ -8,144 +8,83 @@ $admin = isset($mod["type"]) && $mod["type"]<=30;
if (php_sapi_name() == 'fpm-fcgi' && !$admin) { if (php_sapi_name() == 'fpm-fcgi' && !$admin) {
error('Cannot be run directly.'); error('Cannot be run directly.');
} }
$boards = listBoards();
$all_tags = array();
$total_posts_hour = 0;
$total_posts = 0;
$write_maxes = false;
function to_tag($str) { /* Build parameters for page */
$str = trim($str); $searchJson = include "board-search.php";
$str = strtolower($str); $boards = array();
$str = str_replace(['_', ' '], '-', $str); $tags = array();
return $str;
if (count($searchJson)) {
if (isset($searchJson['boards'])) {
$boards = $searchJson['boards'];
}
if (isset($searchJson['tags'])) {
$tags = $searchJson['tags'];
}
} }
if (!file_exists('maxes.txt') || filemtime('maxes.txt') < (time() - (60*60))) { /* $query = prepare(sprintf("
$fp = fopen('maxes.txt', 'w+');
$write_maxes = true;
}
foreach ($boards as $i => $board) {
$query = prepare(sprintf("
SELECT IFNULL(MAX(id),0) max, SELECT IFNULL(MAX(id),0) max,
(SELECT COUNT(*) FROM ``posts_%s`` WHERE FROM_UNIXTIME(time) > DATE_SUB(NOW(), INTERVAL 1 HOUR)) pph, (SELECT COUNT(*) FROM ``posts_%s`` WHERE FROM_UNIXTIME(time) > DATE_SUB(NOW(), INTERVAL 1 HOUR)) pph,
(SELECT COUNT(DISTINCT ip) FROM ``posts_%s`` WHERE FROM_UNIXTIME(time) > DATE_SUB(NOW(), INTERVAL 3 DAY)) uniq_ip (SELECT COUNT(DISTINCT ip) FROM ``posts_%s`` WHERE FROM_UNIXTIME(time) > DATE_SUB(NOW(), INTERVAL 3 DAY)) uniq_ip
FROM ``posts_%s`` FROM ``posts_%s``
", $board['uri'], $board['uri'], $board['uri'], $board['uri'], $board['uri'])); ", $board['uri'], $board['uri'], $board['uri'], $board['uri'], $board['uri']));
$query->execute() or error(db_error($query)); $query->execute() or error(db_error($query));
$r = $query->fetch(PDO::FETCH_ASSOC); $r = $query->fetch(PDO::FETCH_ASSOC); */
$tquery = prepare("SELECT `tag` FROM ``board_tags`` WHERE `uri` = :uri"); $boardQuery = prepare("SELECT COUNT(1) AS 'boards_total', COUNT(indexed) AS 'boards_public' FROM ``boards``");
$tquery->execute([":uri" => $board['uri']]) or error(db_error($tquery)); $boardQuery->execute() or error(db_error($tagQuery));
$r2 = $tquery->fetchAll(PDO::FETCH_ASSOC); $boardResult = $boardQuery->fetchAll(PDO::FETCH_ASSOC)[0];
$tags = array(); $boards_total = $boardResult['boards_total'];
if ($r2) { $boards_public = $boardResult['boards_public'];
foreach ($r2 as $ii => $t) { $boards_hidden = $boardResult['boards_total'] - $boardResult['boards_public'];
$tag=to_tag($t['tag']);
$tags[] = $tag;
if (!isset($all_tags[$tag])) {
$all_tags[$tag] = (int)$r['uniq_ip'];
} else {
$all_tags[$tag] += $r['uniq_ip'];
}
}
}
$pph = $r['pph']; $posts_hour = 0;
$posts_total = 0;
$total_posts_hour += $pph; /* Create and distribute page */
$total_posts += $r['max']; $boardsHTML = Element("8chan/boards-table.html", array(
"config" => $config,
"boards" => $boards,
)
);
$boards[$i]['pph'] = $pph; $tagsHTML = Element("8chan/boards-tags.html", array(
$boards[$i]['ppd'] = $pph*24; "config" => $config,
$boards[$i]['max'] = $r['max']; "tags" => $tags,
$boards[$i]['uniq_ip'] = $r['uniq_ip']; )
$boards[$i]['tags'] = $tags; );
if ($write_maxes) fwrite($fp, $board['uri'] . ':' . $boards[$i]['max'] . "\n"); $searchHTML = Element("8chan/boards-search.html", array(
} "config" => $config,
if ($write_maxes) fclose($fp);
"boards_total" => $boards_total,
"boards_public" => $boards_public,
"boards_hidden" => $boards_hidden,
"posts_hour" => $posts_hour,
"posts_total" => $posts_total,
"page_updated" => date('r'),
"uptime" => shell_exec('uptime -p'),
"html_boards" => $boardsHTML,
"html_tags" => $tagsHTML
)
);
usort($boards, $config['additional_javascript'] = array(
function ($a, $b) { 'js/jquery.min.js',
$x = $b['uniq_ip'] - $a['uniq_ip']; 'js/board-directory.js'
if ($x) { return $x; );
//} else { return strcmp($a['uri'], $b['uri']); }
} else { return $b['max'] - $a['max']; }
});
$hidden_boards_total = 0; $pageHTML = Element("page.html", array(
$rows = array(); "config" => $config,
foreach ($boards as $i => &$board) { "body" => $searchHTML,
$board_config = @file_get_contents($board['uri'].'/config.php'); "title" => "Boards on &infin;chan"
$boardCONFIG = array(); )
if ($board_config && $board['uri'] !== 'int') { );
$board_config = str_replace('$config', '$boardCONFIG', $board_config);
$board_config = str_replace('<?php', '', $board_config);
@eval($board_config);
}
$showboard = $board['indexed'];
$locale = isset($boardCONFIG['locale'])?$boardCONFIG['locale']:'en';
$board['title'] = utf8tohtml($board['title']); file_write("boards.html", $pageHTML);
$locale_arr = explode('_', $locale); echo $pageHTML;
$locale_short = isset($locale_arr[1]) ? strtolower($locale_arr[1]) : strtolower($locale_arr[0]);
$locale_short = str_replace('.utf-8', '', $locale_short);
$country = get_country($locale_short);
if ($board['uri'] === 'int') {$locale_short = 'eo'; $locale = 'eo'; $country = 'Esperanto';}
$board['img'] = "<img class=\"flag flag-$locale_short\" src=\"/static/blank.gif\" style=\"width:16px;height:11px;\" alt=\"$country\" title=\"$country\">";
if ($showboard || $admin) {
if (!$showboard) {
$lock = ' <i class="fa fa-lock" title="No index"></i>';
} else {
$lock = '';
}
$board['ago'] = human_time_diff(strtotime($board['time']));
} else {
unset($boards[$i]);
$hidden_boards_total += 1;
}
}
$n_boards = sizeof($boards);
$t_boards = $hidden_boards_total + $n_boards;
$boards = array_values($boards);
arsort($all_tags);
$config['additional_javascript'] = array('js/jquery.min.js', 'js/jquery.tablesorter.min.js');
$body = Element("8chan/boards-tags.html", array("config" => $config, "n_boards" => $n_boards, "t_boards" => $t_boards, "hidden_boards_total" => $hidden_boards_total, "total_posts" => $total_posts, "total_posts_hour" => $total_posts_hour, "boards" => $boards, "last_update" => date('r'), "uptime_p" => shell_exec('uptime -p'), 'tags' => $all_tags, 'top2k' => false));
$html = Element("page.html", array("config" => $config, "body" => $body, "title" => "Boards on &infin;chan"));
$boards_top2k = $boards;
array_splice($boards_top2k, 100);
$boards_top2k = array_values($boards_top2k);
$body = Element("8chan/boards-tags.html", array("config" => $config, "n_boards" => $n_boards, "t_boards" => $t_boards, "hidden_boards_total" => $hidden_boards_total, "total_posts" => $total_posts, "total_posts_hour" => $total_posts_hour, "boards" => $boards_top2k, "last_update" => date('r'), "uptime_p" => shell_exec('uptime -p'), 'tags' => $all_tags, 'top2k' => true));
$html_top2k = Element("page.html", array("config" => $config, "body" => $body, "title" => "Boards on &infin;chan"));
if ($admin) {
echo $html;
} else {
foreach ($boards as $i => &$b) { unset($b['img']); }
file_write("boards.json", json_encode($boards));
file_write("tags.json", json_encode($all_tags));
foreach ($boards as $i => $b) {/*
if (in_array($b['uri'], $config['no_top_bar_boards'])) {
unset($boards[$i]);
}*/
unset($boards[$i]['img']);
}
array_splice($boards, 48);
$boards = array_values($boards);
file_write("boards-top20.json", json_encode($boards));
file_write("boards.html", $html_top2k);
file_write("boards_full.html", $html);
echo $html;
}

View File

@ -820,11 +820,14 @@ function loadBoardConfig( $uri ) {
} }
function fetchBoardActivity( $uris ) { function fetchBoardActivity( $uris ) {
global $config;
$boardActivity = array(); $boardActivity = array();
/* $tablePrefix = "{$config['db']['prefix']}posts_";
$uris = "\"" . implode( (array) $uris, "\",\"" ) . "\""; $uris = "\"{$tablePrefix}" . implode( (array) $uris, "\",\"{$tablePrefix}" ) . "\"";
/*
$tagQuery = prepare("SELECT * FROM ``board_tags`` WHERE `uri` IN ({$uris})"); $tagQuery = prepare("SELECT * FROM ``board_tags`` WHERE `uri` IN ({$uris})");
$tagQuery->execute() or error(db_error($tagQuery)); $tagQuery->execute() or error(db_error($tagQuery));
$tagResult = $tagQuery->fetchAll(PDO::FETCH_ASSOC); $tagResult = $tagQuery->fetchAll(PDO::FETCH_ASSOC);
@ -845,20 +848,30 @@ function fetchBoardActivity( $uris ) {
} }
*/ */
foreach( (array) $uris as $uri ) { $aiQuery = prepare("SELECT `TABLE_NAME`, `AUTO_INCREMENT` FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"{$config['db']['database']}\" AND TABLE_NAME IN ({$uris})");
$aiQuery->execute() or error(db_error($aiQuery));
$aiResult = $aiQuery->fetchAll(PDO::FETCH_ASSOC);
foreach ($aiResult as $aiRow) {
$uri = str_replace( $tablePrefix, "", $aiRow['TABLE_NAME'] );
$posts = $aiRow['AUTO_INCREMENT'] - 1;
$random = rand( -1000, 1000 ); $random = rand( -1000, 1000 );
if( $random < 0 ) $random = 0; if( $random < 0 ) $random = 0;
$boardActivity['active'][ $uri ] = $random; $boardActivity['active'][ $uri ] = $random;
$boardActivity['average'][ $uri ] = $random * 72; $boardActivity['average'][ $uri ] = ($random * 72) / 72;
$boardActivity['posts'][ $uri ] = $posts;
} }
return $boardActivity; return $boardActivity;
} }
function fetchBoardTags( $uris ) { function fetchBoardTags( $uris ) {
global $config;
$boardTags = array(); $boardTags = array();
$uris = "\"" . implode( (array) $uris, "\",\"" ) . "\""; $uris = "\"{$config['db']['prefix']}" . implode( (array) $uris, "\",\"" ) . "\"";
$tagQuery = prepare("SELECT * FROM ``board_tags`` WHERE `uri` IN ({$uris})"); $tagQuery = prepare("SELECT * FROM ``board_tags`` WHERE `uri` IN ({$uris})");
$tagQuery->execute() or error(db_error($tagQuery)); $tagQuery->execute() or error(db_error($tagQuery));

View File

@ -1394,10 +1394,10 @@ aside.search-container .box {
margin: 8px 0; margin: 8px 0;
} }
.search-sfw { .search-sfw {
display: block;
cursor: pointer; cursor: pointer;
font-size: 110%; font-size: 110%;
line-height: 120%; line-height: 120%;
vertical-align: bottom;
} }
#search-sfw-input { #search-sfw-input {
margin: 0; margin: 0;
@ -1405,6 +1405,7 @@ aside.search-container .box {
transform: scale(1.20); transform: scale(1.20);
} }
#search-lang-input, #search-lang-input,
#search-title-input,
#search-tag-input { #search-tag-input {
box-sizing: border-box; box-sizing: border-box;
font-size: 110%; font-size: 110%;

View File

@ -0,0 +1,78 @@
<main id="boardlist">
<section class="description box col col-12">
<h2 class="box-title">Global Statistics</h2>
<p class="box-content">{% trans %}There are currently <strong>{{boards_public}}</strong> public boards, <strong>{{boards_total}}</strong> total. Site-wide, {{posts_hour}} posts have been made in the last hour, with {{posts_total}} being made on all active boards since {{founding_date}}.{% endtrans %}</p>
{% if uptime %}<p class="box-content">{{uptime}} without interruption</p>{% endif %}
<p class="box-content">This page last updated {{page_updated}}.</p>
</section>
<div class="board-list">
<aside class="search-container col col-2">
<form id="search-form" class="box" method="post" target="/board-search.php">
<h2 class="box-title">Search</h2>
<div class="board-search box-content">
<label class="search-item search-sfw">
<input type="checkbox" id="search-sfw-input" name="sfw" checked="checked" />&nbsp;NSFW boards
</label>
<div class="search-item search-title">
<input type="text" id="search-title-input" name="title" placeholder="Search titles..." />
</div>
<div class="search-item search-lang">
<select id="search-lang-input" name="lang">
<optgroup label="Popular">
<option>All languages</option>
<option>English</option>
<option>Spanish</option>
</optgroup>
<optgroup label="All">
<option>Chinese</option>
</optgroup>
</select>
</div>
<div class="search-item search-tag">
<input type="text" id="search-tag-input" name="tag" placeholder="Search tags..." />
</div>
<div class="search-item search-submit">
<button id="search-submit">Search</button>
</div>
</div>
<ul class="tag-list box-content">
{{html_tags}}
</ul>
</form>
</aside>
<section class="board-list col col-10">
<table class="board-list-table">
<colgroup>
<col class="board-meta" />
<col class="board-uri" />
<col class="board-title" />
<col class="board-pph" />
<col class="board-max" />
<col class="board-unique" />
<col class="board-tags" />
</colgroup>
<thead>
<tr>
<th class="board-meta" data-column="meta"></th>
<th class="board-uri" data-column="uri">{% trans %}Board{% endtrans %}</th>
<th class="board-title" data-column="title">{% trans %}Title{% endtrans %}</th>
<th class="board-pph" data-column="pph" title="Posts per hour">{% trans %}PPH{% endtrans %}</th>
<th class="board-max" data-column="max">{% trans %}Total posts{% endtrans %}</th>
<th class="board-unique" data-column="unique" title="Unique IPs to post in the last 72 hours">{% trans %}Active users{% endtrans %}</th>
<th class="board-tags" data-column="tags">{% trans %}Tags{% endtrans %}</th>
</tr>
</thead>
<tbody class="board-list-tbody">{{html_boards}}</tbody>
</table>
</section>
</div>
</main>

View File

@ -0,0 +1,11 @@
{% for board in boards %}
<tr>
<td class="board-meta">{{ board.img|raw }} {% if board['sfw'] %}<img src="/static/sfw.png" title="Safe for work">{% else %}<img src="/static/nsfw.png" title="Not safe for work">{% endif %}</td>
<td class="board-uri"><div class="board-list-wrapper uri"><a href='/{{board['uri']}}/'>/{{board['uri']}}/</a>{{lock|raw}}</div></td>
<td class="board-title"><div class="board-cell" title="Created {{board['time']}} ({{board['ago']}} ago)">{{ board['title'] }}</div></td>
<td class="board-pph"><div class="board-cell">{{board['pph']}}</td>
<td class="board-max"><div class="board-cell">{{board['posts']}}</td>
<td class="board-unique"><div class="board-cell">{{board['active']}}</td>
<td class="board-tags"><div class="board-cell">{% for tag in board.tags %}<span class="board-tag">{{ tag }}</span>&nbsp;{% endfor %}</div></td>
</tr>
{% endfor %}

View File

@ -1,90 +1,5 @@
<main id="boardlist"> {% for tag, weight in tags %}
<section class="description box col col-12"> <li class="tag-item">
<h2 class="box-title">Global Statistics</h2> <a class="tag-link" href="#">{{tag}}</a>
<p class="box-content">{% trans %}There are currently <strong>{{t_boards}}</strong> total boards, <strong>{{hidden_boards_total}}</strong> of which are unindexed. Site-wide, {{total_posts_hour}} posts have been made in the last hour, with {{total_posts}} being made on all active boards since October 23, 2013.{% endtrans %}</p> </li>
{% if uptime %}<p class="box-content">{{uptime_p}} without interruption</p>{% endif %} {% endfor %}
<p class="box-content">This page last updated <time>{{last_update}}</time>.</p>
</section>
<div class="board-list">
<aside class="search-container col col-2">
<form id="search-form" class="box" method="post" target="/board-search.php">
<h2 class="box-title">Search</h2>
<div class="board-search box-content">
<label class="search-item search-sfw">
<input type="checkbox" id="search-sfw-input" checked="checked" />&nbsp;NSFW boards
</label>
<div class="search-item search-lang">
<select id="search-lang-input">
<optgroup label="Popular">
<option>All languages</option>
<option>English</option>
<option>Spanish</option>
</optgroup>
<optgroup label="All">
<option>Chinese</option>
</optgroup>
</select>
</div>
<div class="search-item search-tag">
<input type="text" id="search-tag-input" placeholder="Search tags..." />
</div>
<div class="search-item search-submit">
<button id="search-submit">Search</button>
</div>
</div>
<ul class="tag-list box-content">
{% for tag, pop in tags %}
<li class="tag-item">
<a class="tag-link" href="#">{{ tag }}</a>
</li>
{% endfor %}
</ul>
</form>
</aside>
<section class="board-list col col-10">
<table class="board-list-table">
<colgroup>
<col class="board-meta" />
<col class="board-uri" />
<col class="board-title" />
<col class="board-pph" />
<col class="board-max" />
<col class="board-unique" />
<col class="board-tags" />
</colgroup>
<thead>
<tr>
<th class="board-meta"></th>
<th class="board-uri">{% trans %}Board{% endtrans %}</th>
<th class="board-title">{% trans %}Title{% endtrans %}</th>
<th class="board-pph" title="Posts per hour">{% trans %}PPH{% endtrans %}</th>
<th class="board-max">{% trans %}Total posts{% endtrans %}</th>
<th class="board-unique" title="Unique IPs to post in the last 72 hours">{% trans %}Active users{% endtrans %}</th>
<th class="board-tags">{% trans %}Tags{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for board in boards %}
<tr>
<td class="board-meta">{{ board.img|raw }} {% if board['sfw'] %}<img src="/static/sfw.png" title="Safe for work">{% else %}<img src="/static/nsfw.png" title="Not safe for work">{% endif %}</td>
<td class="board-uri"><div class="board-list-wrapper uri"><a href='/{{board['uri']}}/'>/{{board['uri']}}/</a>{{lock|raw}}</div></td>
<td class="board-title"><div class="board-cell" title="Created {{board['time']}} ({{board['ago']}} ago)">{{ board['title'] }}</div></td>
<td class="board-pph"><div class="board-cell">{{board['pph']}}</td>
<td class="board-max"><div class="board-cell">{{board['max']}}</td>
<td class="board-unique"><div class="board-cell">{{board['uniq_ip']}}</td>
<td class="board-tags"><div class="board-cell">{% for tag in board.tags %}<span class="board-tag">{{ tag }}</span>&nbsp;{% endfor %}</div></td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
</div>
</main>