The number of unread alerts now appears in the page title.

Added the Back to Site link to Nox's Control Panel.
Added the panel_group_menu template and used it to de-dupe the group menu HTML.
Fixed a potential race condition with dismiss alert.
Fixed a strange bug where new alerts wouldn't appear.
Fixed a race condition where client templates sometimes wouldn't load.
Dramatically cut down on the number of DOM rebuilds for the alert list.
Added some missing error handling for ajax page block loads.
Fixed a bug where the dimiss alert endpoint wasn't sending a success payload.

Made the register_might_be_machine phrase more descriptive.
Added the panel_menu_aria phrase.
This commit is contained in:
Azareal 2019-03-16 21:31:10 +10:00
parent 414d9c4817
commit cb58c1c83f
14 changed files with 226 additions and 67 deletions

View File

@ -224,7 +224,7 @@ func (list SFileList) JSTmplInit() error {
for name, _ := range Themes { for name, _ := range Themes {
if strings.HasSuffix(shortName, "_"+name) { if strings.HasSuffix(shortName, "_"+name) {
data = append(data, "\nlet Template_"+strings.TrimSuffix(shortName, "_"+name)+" = Template_"+shortName+";"...) data = append(data, "\nvar Template_"+strings.TrimSuffix(shortName, "_"+name)+" = Template_"+shortName+";"...)
break break
} }
} }

View File

@ -90,7 +90,7 @@
"id_must_be_integer": "The ID must be an integer.", "id_must_be_integer": "The ID must be an integer.",
"url_id_must_be_integer": "The ID in the URL needs to be a valid integer.", "url_id_must_be_integer": "The ID in the URL needs to be a valid integer.",
"register_might_be_machine":"You might be a machine.", "register_might_be_machine":"Our algorithms have detected that you may be a machine. If not, please try to avoid acting so quickly.",
"register_need_username":"You didn't put in a username.", "register_need_username":"You didn't put in a username.",
"register_need_email":"You didn't put in an email.", "register_need_email":"You didn't put in an email.",
"register_first_word_numeric":"The first word of your name must not be purely numeric", "register_first_word_numeric":"The first word of your name must not be purely numeric",
@ -709,6 +709,7 @@
"option_no":"No", "option_no":"No",
"panel_menu_head":"Control Panel", "panel_menu_head":"Control Panel",
"panel_menu_aria":"The control panel menu",
"panel_menu_users":"Users", "panel_menu_users":"Users",
"panel_menu_groups":"Groups", "panel_menu_groups":"Groups",
"panel_menu_forums":"Forums", "panel_menu_forums":"Forums",

View File

@ -7,6 +7,7 @@ var moreTopicCount = 0;
var conn = false; var conn = false;
var selectedTopics = []; var selectedTopics = [];
var attachItemCallback = function(){} var attachItemCallback = function(){}
var baseTitle = document.title;
// Topic move // Topic move
var forumToMoveTo = 0; var forumToMoveTo = 0;
@ -17,9 +18,7 @@ function ajaxError(xhr,status,errstr) {
console.log("xhr", xhr); console.log("xhr", xhr);
console.log("status", status); console.log("status", status);
console.log("errstr", errstr); console.log("errstr", errstr);
if(status=="parsererror") { if(status=="parsererror") console.log("The server didn't respond with a valid JSON response");
console.log("The server didn't respond with a valid JSON response");
}
console.trace(); console.trace();
} }
@ -30,19 +29,28 @@ function postLink(event) {
} }
function bindToAlerts() { function bindToAlerts() {
console.log("bindToAlerts");
$(".alertItem.withAvatar a").unbind("click"); $(".alertItem.withAvatar a").unbind("click");
$(".alertItem.withAvatar a").click(function(event) { $(".alertItem.withAvatar a").click(function(event) {
event.stopPropagation(); event.stopPropagation();
$.ajax({ url: "/api/?action=set&module=dismiss-alert", type: "POST", dataType: "json", error: ajaxError, data: { asid: $(this).attr("data-asid") } }); event.preventDefault();
$.ajax({
url: "/api/?action=set&module=dismiss-alert",
type: "POST",
dataType: "json",
data: { asid: $(this).attr("data-asid") },
error: ajaxError,
success: () => {
window.location.href = this.getAttribute("href");
}
});
}); });
} }
function addAlert(msg, notice = false) { function addAlert(msg, notice = false) {
var mmsg = msg.msg; var mmsg = msg.msg;
if("sub" in msg) { if("sub" in msg) {
for(var i = 0; i < msg.sub.length; i++) { for(var i = 0; i < msg.sub.length; i++) mmsg = mmsg.replace("\{"+i+"\}", msg.sub[i]);
mmsg = mmsg.replace("\{"+i+"\}", msg.sub[i]);
}
} }
let aItem = Template_alert({ let aItem = Template_alert({
@ -51,7 +59,10 @@ function addAlert(msg, notice = false) {
Avatar: msg.avatar || "", Avatar: msg.avatar || "",
Message: mmsg Message: mmsg
}) })
alertMapping[msg.asid] = aItem; //alertMapping[msg.asid] = aItem;
let div = document.createElement('div');
div.innerHTML = aItem.trim();
alertMapping[msg.asid] = div.firstChild;
alertList.push(msg.asid); alertList.push(msg.asid);
if(notice) { if(notice) {
@ -74,21 +85,31 @@ function updateAlertList(menuAlerts) {
let alertCounterNode = menuAlerts.getElementsByClassName("alert_counter")[0]; let alertCounterNode = menuAlerts.getElementsByClassName("alert_counter")[0];
alertCounterNode.textContent = "0"; alertCounterNode.textContent = "0";
let outList = ""; alertListNode.innerHTML = "";
let any = false;
/*let outList = "";
let j = 0; let j = 0;
for(var i = 0; i < alertList.length && j < 8; i++) { for(var i = 0; i < alertList.length && j < 8; i++) {
outList += alertMapping[alertList[i]]; outList += alertMapping[alertList[i]];
j++; j++;
}*/
let j = 0;
for(var i = 0; i < alertList.length && j < 8; i++) {
any = true;
alertListNode.appendChild(alertMapping[alertList[i]]);
//outList += alertMapping[alertList[i]];
j++;
} }
if(!any) alertListNode.innerHTML = "<div class='alertItem'>"+phraseBox["alerts"]["alerts.no_alerts"]+"</div>";
if(outList == "") outList = "<div class='alertItem'>"+phraseBox["alerts"]["alerts.no_alerts"]+"</div>";
alertListNode.innerHTML = outList;
if(alertCount != 0) { if(alertCount != 0) {
alertCounterNode.textContent = alertCount; alertCounterNode.textContent = alertCount;
menuAlerts.classList.add("has_alerts"); menuAlerts.classList.add("has_alerts");
let nTitle = "("+alertCount+") "+baseTitle;
if(document.title!=nTitle) document.title = nTitle;
} else { } else {
menuAlerts.classList.remove("has_alerts"); menuAlerts.classList.remove("has_alerts");
if(document.title!=baseTitle) document.title = baseTitle;
} }
bindToAlerts(); bindToAlerts();
@ -155,14 +176,20 @@ function SplitN(data,ch,n) {
} }
function wsAlertEvent(data) { function wsAlertEvent(data) {
console.log("wsAlertEvent:",data)
addAlert(data, true); addAlert(data, true);
alertCount++;
var alist = ""; let aTmp = alertList;
for (var i = 0; i < alertList.length; i++) alist += alertMapping[alertList[i]]; alertList = [alertList[alertList.length-1]];
aTmp = aTmp.slice(0,-1);
for(let i = 0; i < aTmp.length; i++) alertList.push(aTmp[i]);
//var alist = "";
//for (var i = 0; i < alertList.length; i++) alist += alertMapping[alertList[i]];
// TODO: Add support for other alert feeds like PM Alerts // TODO: Add support for other alert feeds like PM Alerts
var generalAlerts = document.getElementById("general_alerts"); var generalAlerts = document.getElementById("general_alerts");
// TODO: Make sure we update alertCount here // TODO: Make sure we update alertCount here
updateAlertList(generalAlerts, alist); updateAlertList(generalAlerts/*, alist*/);
} }
function runWebSockets() { function runWebSockets() {
@ -179,9 +206,7 @@ function runWebSockets() {
console.log("The WebSockets connection was opened"); console.log("The WebSockets connection was opened");
conn.send("page " + document.location.pathname + '\r'); conn.send("page " + document.location.pathname + '\r');
// TODO: Don't ask again, if it's denied. We could have a setting in the UCP which automatically requests this when someone flips desktop notifications on // TODO: Don't ask again, if it's denied. We could have a setting in the UCP which automatically requests this when someone flips desktop notifications on
if(me.User.ID > 0) { if(me.User.ID > 0) Notification.requestPermission();
Notification.requestPermission();
}
} }
conn.onclose = () => { conn.onclose = () => {
@ -291,8 +316,13 @@ function runWebSockets() {
(() => { (() => {
addInitHook("pre_init", () => { addInitHook("pre_init", () => {
console.log("before notify on alert")
// We can only get away with this because template_alert has no phrases, otherwise it too would have to be part of the "dance", I miss Go concurrency :( // We can only get away with this because template_alert has no phrases, otherwise it too would have to be part of the "dance", I miss Go concurrency :(
notifyOnScriptW("/static/template_alert", () => {}, () => { notifyOnScriptW("template_alert", (e) => {
if(e!=undefined) console.log("failed alert? why?", e)
}, () => {
console.log("ha")
if(!Template_alert) throw("template function not found");
addInitHook("after_phrases", () => { addInitHook("after_phrases", () => {
// TODO: The load part of loadAlerts could be done asynchronously while the update of the DOM could be deferred // TODO: The load part of loadAlerts could be done asynchronously while the update of the DOM could be deferred
$(document).ready(() => { $(document).ready(() => {
@ -438,10 +468,13 @@ function mainInit(){
// TODO: Try to de-duplicate some of these fetch calls // TODO: Try to de-duplicate some of these fetch calls
fetch(url+q+"&js=1", {credentials: "same-origin"}) fetch(url+q+"&js=1", {credentials: "same-origin"})
.then((resp) => resp.json()) .then((resp) => {
.then((data) => { if(!resp.ok) throw(url+q+"&js=1 failed to load");
return resp.json();
}).then((data) => {
if(!"Topics" in data) throw("no Topics in data"); if(!"Topics" in data) throw("no Topics in data");
let topics = data["Topics"]; let topics = data["Topics"];
console.log("ajax navigated to different page");
// TODO: Fix the data race where the function hasn't been loaded yet // TODO: Fix the data race where the function hasn't been loaded yet
let out = ""; let out = "";
@ -470,10 +503,14 @@ function mainInit(){
let url = "//"+window.location.host+"/topics/?fids="+fid; let url = "//"+window.location.host+"/topics/?fids="+fid;
fetch(url+"&js=1", {credentials: "same-origin"}) fetch(url+"&js=1", {credentials: "same-origin"})
.then((resp) => resp.json()) .then((resp) => {
.then((data) => { if(!resp.ok) throw(url+"&js=1 failed to load");
return resp.json();
}).then((data) => {
console.log("data:",data);
if(!"Topics" in data) throw("no Topics in data"); if(!"Topics" in data) throw("no Topics in data");
let topics = data["Topics"]; let topics = data["Topics"];
console.log("ajax navigated to "+that.innerText);
// TODO: Fix the data race where the function hasn't been loaded yet // TODO: Fix the data race where the function hasn't been loaded yet
let out = ""; let out = "";
@ -481,6 +518,9 @@ function mainInit(){
$(".topic_list").html(out); $(".topic_list").html(out);
//$(".topic_list").addClass("single_forum"); //$(".topic_list").addClass("single_forum");
baseTitle = that.innerText;
if(alertCount > 0) document.title = "("+alertCount+") "+baseTitle;
else document.title = baseTitle;
let obj = {Title: document.title, Url: url}; let obj = {Title: document.title, Url: url};
history.pushState(obj, obj.Title, obj.Url); history.pushState(obj, obj.Title, obj.Url);
rebuildPaginator(data.LastPage) rebuildPaginator(data.LastPage)
@ -515,18 +555,23 @@ function mainInit(){
// TODO: Try to de-duplicate some of these fetch calls // TODO: Try to de-duplicate some of these fetch calls
fetch(url+q+"&js=1", {credentials: "same-origin"}) fetch(url+q+"&js=1", {credentials: "same-origin"})
.then((resp) => resp.json()) .then((resp) => {
.then((data) => { if(!resp.ok) throw(url+q+"&js=1 failed to load");
return resp.json();
}).then((data) => {
if(!"Topics" in data) throw("no Topics in data"); if(!"Topics" in data) throw("no Topics in data");
let topics = data["Topics"]; let topics = data["Topics"];
console.log("ajax navigated to search page");
// TODO: Fix the data race where the function hasn't been loaded yet // TODO: Fix the data race where the function hasn't been loaded yet
let out = ""; let out = "";
for(let i = 0; i < topics.length;i++) out += Template_topics_topic(topics[i]); for(let i = 0; i < topics.length;i++) out += Template_topics_topic(topics[i]);
$(".topic_list").html(out); $(".topic_list").html(out);
document.title = phraseBox["topic_list"]["topic_list.search_head"]; baseTitle = phraseBox["topic_list"]["topic_list.search_head"];
$(".topic_list_title h1").text(phraseBox["topic_list"]["topic_list.search_head"]); $(".topic_list_title h1").text(phraseBox["topic_list"]["topic_list.search_head"]);
if(alertCount > 0) document.title = "("+alertCount+") "+baseTitle;
else document.title = baseTitle;
let obj = {Title: document.title, Url: url+q}; let obj = {Title: document.title, Url: url+q};
history.pushState(obj, obj.Title, obj.Url); history.pushState(obj, obj.Title, obj.Url);
rebuildPaginator(data.LastPage); rebuildPaginator(data.LastPage);

View File

@ -81,23 +81,30 @@ function asyncGetScript(source) {
} }
function notifyOnScript(source) { function notifyOnScript(source) {
source = "/static/"+source;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let ss = source.replace("/static/","");
try {
let ssp = ss.charAt(0).toUpperCase() + ss.slice(1)
console.log("ssp:",ssp)
if(window[ssp]) {
resolve();
return;
}
} catch(e) {}
console.log("source:",source)
let script = document.querySelectorAll('[src^="'+source+'"]')[0]; let script = document.querySelectorAll('[src^="'+source+'"]')[0];
console.log("script:",script);
if(script===undefined) { if(script===undefined) {
reject("no script found"); reject("no script found");
return; return;
} }
if(!script.readyState) {
resolve();
return;
}
const onloadHandler = (e, isAbort) => { const onloadHandler = (e) => {
if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
script.onload = null; script.onload = null;
script.onreadystatechange = null; script.onreadystatechange = null;
isAbort ? reject(e) : resolve(); resolve();
}
} }
script.onerror = (e) => { script.onerror = (e) => {
@ -105,7 +112,6 @@ function notifyOnScript(source) {
}; };
script.onload = onloadHandler; script.onload = onloadHandler;
script.onreadystatechange = onloadHandler; script.onreadystatechange = onloadHandler;
script.src = source;
}); });
} }
@ -119,7 +125,7 @@ function notifyOnScriptW(name, complete, success) {
console.log("Unable to get script name '"+name+"'"); console.log("Unable to get script name '"+name+"'");
console.log("e: ", e); console.log("e: ", e);
console.trace(); console.trace();
complete(); complete(e);
}); });
} }
@ -168,6 +174,7 @@ function RelativeTime(date) {
function initPhrases() { function initPhrases() {
console.log("in initPhrases") console.log("in initPhrases")
console.log("tmlInits:",tmplInits)
fetchPhrases("status,topic_list,alerts,paginator,analytics") fetchPhrases("status,topic_list,alerts,paginator,analytics")
} }
@ -206,11 +213,13 @@ function fetchPhrases(plist) {
runInitHook("pre_iife"); runInitHook("pre_iife");
let toLoad = 2; let toLoad = 2;
// TODO: Shunt this into loggedIn if there aren't any search and filter widgets? // TODO: Shunt this into loggedIn if there aren't any search and filter widgets?
notifyOnScriptW("/static/template_topics_topic", () => { notifyOnScriptW("template_topics_topic", () => {
if(!Template_topics_topic) throw("template function not found");
toLoad--; toLoad--;
if(toLoad===0) initPhrases(); if(toLoad===0) initPhrases();
}); });
notifyOnScriptW("/static/template_paginator", () => { notifyOnScriptW("template_paginator", () => {
if(!Template_paginator) throw("template function not found");
toLoad--; toLoad--;
if(toLoad===0) initPhrases(); if(toLoad===0) initPhrases();
}); });

View File

@ -63,6 +63,7 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R
if common.EnableWebsockets && count > 0 { if common.EnableWebsockets && count > 0 {
_ = common.WsHub.PushMessage(user.ID, `{"event":"dismiss-alert","asid":`+strconv.Itoa(asid)+`}`) _ = common.WsHub.PushMessage(user.ID, `{"event":"dismiss-alert","asid":`+strconv.Itoa(asid)+`}`)
} }
w.Write(successJSONBytes)
// TODO: Split this into it's own function // TODO: Split this into it's own function
case "alerts": // A feed of events tailored for a specific user case "alerts": // A feed of events tailored for a specific user
if !user.Loggedin { if !user.Loggedin {

View File

@ -778,8 +778,7 @@ func AccountPasswordReset(w http.ResponseWriter, r *http.Request, user common.Us
header.AddNotice("password_reset_email_sent") header.AddNotice("password_reset_email_sent")
} }
header.Title = phrases.GetTitlePhrase("password_reset") header.Title = phrases.GetTitlePhrase("password_reset")
pi := common.Page{header, tList, nil} return renderTemplate("password_reset", w, r, header, common.Page{header, tList, nil})
return renderTemplate("password_reset", w, r, header, pi)
} }
// TODO: Ratelimit this // TODO: Ratelimit this

View File

@ -1,16 +1,6 @@
{{template "header.html" . }} {{template "header.html" . }}
<div class="colstack panel_stack"> <div class="colstack panel_stack">
<nav class="colstack_left" aria-label="The control panel menu"> {{template "panel_group_menu.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_head"}}</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_general"}}</a></div>
<div class="rowitem passive"><a>{{lang "panel_group_menu_promotions"}}</a></div>
<div class="rowitem passive"><a href="/panel/groups/edit/perms/{{.ID}}">{{lang "panel_group_menu_permissions"}}</a></div>
</div>
{{template "panel_inner_menu.html" . }}
</nav>
<main class="colstack_right"> <main class="colstack_right">
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Name}}{{lang "panel_group_head_suffix"}}</h1></div> <div class="rowitem"><h1>{{.Name}}{{lang "panel_group_head_suffix"}}</h1></div>

View File

@ -1,16 +1,6 @@
{{template "header.html" . }} {{template "header.html" . }}
<div class="colstack panel_stack"> <div class="colstack panel_stack">
<nav class="colstack_left" aria-label="The control panel menu"> {{template "panel_group_menu.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_head"}}</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_general"}}</a></div>
<div class="rowitem passive"><a>{{lang "panel_group_menu_promotions"}}</a></div>
<div class="rowitem passive"><a href="/panel/groups/edit/perms/{{.ID}}">{{lang "panel_group_menu_permissions"}}</a></div>
</div>
{{template "panel_inner_menu.html" . }}
</nav>
<main class="colstack_right"> <main class="colstack_right">
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Name}}{{lang "panel_group_head_suffix"}}</h1></div> <div class="rowitem"><h1>{{.Name}}{{lang "panel_group_head_suffix"}}</h1></div>

View File

@ -0,0 +1,11 @@
<nav class="colstack_left" aria-label="{{lang "panel_menu_aria"}}">
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_head"}}</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_general"}}</a></div>
<div class="rowitem passive"><a>{{lang "panel_group_menu_promotions"}}</a></div>
<div class="rowitem passive"><a href="/panel/groups/edit/perms/{{.ID}}">{{lang "panel_group_menu_permissions"}}</a></div>
</div>
{{template "panel_inner_menu.html" . }}
</nav>

View File

@ -1 +1 @@
<nav class="colstack_left" aria-label="The control panel menu">{{template "panel_inner_menu.html" . }}</nav> <nav class="colstack_left" aria-label="{{lang "panel_menu_aria"}}">{{template "panel_inner_menu.html" . }}</nav>

View File

@ -0,0 +1,14 @@
<nav class="colstack_left" aria-label="{{lang "panel_menu_aria"}}">
<div class="colstack_item colstack_head">
<div class="rowitem back_to_site"><a href="/panel/">Back to site</a></div>
</div>
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_head"}}</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_general"}}</a></div>
<div class="rowitem passive"><a>{{lang "panel_group_menu_promotions"}}</a></div>
<div class="rowitem passive"><a href="/panel/groups/edit/perms/{{.ID}}">{{lang "panel_group_menu_permissions"}}</a></div>
</div>
{{template "panel_inner_menu.html" . }}
</nav>

View File

@ -0,0 +1,91 @@
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/">{{lang "panel_menu_head"}}</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive">
<a href="/panel/users/">{{lang "panel_menu_users"}}</a> <a class="menu_stats" href="#">({{.Stats.Users}})</a>
</div>
<div class="rowitem passive">
<a href="/panel/groups/">{{lang "panel_menu_groups"}}</a> <a class="menu_stats" href="#">({{.Stats.Groups}})</a>
</div>
{{if .CurrentUser.Perms.ManageForums}}<div class="rowitem passive">
<a href="/panel/forums/">{{lang "panel_menu_forums"}}</a> <a class="menu_stats" href="#">({{.Stats.Forums}})</a>
</div>{{end}}
<div class="rowitem passive">
<a href="/panel/pages/">{{lang "panel_menu_pages"}}</a> <a class="menu_stats" href="#">({{.Stats.Pages}})</a>
</div>
{{if .CurrentUser.Perms.EditSettings}}<div class="rowitem passive">
<a href="/panel/settings/">{{lang "panel_menu_settings"}}</a> <a class="menu_stats" href="#">({{.Stats.Settings}})</a>
</div>
<div class="rowitem passive">
<a href="/panel/settings/word-filters/">{{lang "panel_menu_word_filters"}}</a> <a class="menu_stats" href="#">({{.Stats.WordFilters}})</a>
</div>{{end}}
{{if .CurrentUser.Perms.ManageThemes}}
<div class="rowitem passive">
<a href="/panel/themes/">{{lang "panel_menu_themes"}}</a> <a class="menu_stats" href="#">({{.Stats.Themes}})</a>
</div>
{{if eq .Zone "themes"}}
<div class="rowitem passive submenu"><a href="/panel/themes/menus/">{{lang "panel_menu_menus"}}</a></div>
<div class="rowitem passive submenu"><a href="/panel/themes/widgets/">{{lang "panel_menu_widgets"}}</a></div>
{{end}}
{{end}}
</div>
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="#">{{lang "panel_menu_events"}}</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive">
<a href="/panel/analytics/views/">{{lang "panel_menu_statistics"}}</a>
</div>
{{if eq .Zone "analytics"}}
<div class="rowitem passive submenu">
<a href="/panel/analytics/posts/">{{lang "panel_menu_statistics_posts"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/topics/">{{lang "panel_menu_statistics_topics"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/forums/">{{lang "panel_menu_statistics_forums"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/routes/">{{lang "panel_menu_statistics_routes"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/agents/">{{lang "panel_menu_statistics_agents"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/systems/">{{lang "panel_menu_statistics_systems"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/langs/">{{lang "panel_menu_statistics_languages"}}</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/referrers/">{{lang "panel_menu_statistics_referrers"}}</a>
</div>
{{end}}
<div class="rowitem passive">
<a href="/forum/{{.ReportForumID}}">{{lang "panel_menu_reports"}}</a> <a class="menu_stats" href="#">({{.Stats.Reports}})</a>
</div>
<div class="rowitem passive">
<a href="/panel/logs/mod/">{{lang "panel_menu_logs"}}</a>
</div>
{{if eq .Zone "logs"}}
<div class="rowitem passive submenu"><a href="/panel/logs/regs/">{{lang "panel_menu_logs_registrations"}}</a></div>
<div class="rowitem passive submenu"><a href="/panel/logs/mod/">{{lang "panel_menu_logs_moderators"}}</a></div>
{{if .CurrentUser.Perms.ViewAdminLogs}}<div class="rowitem passive submenu"><a>{{lang "panel_menu_logs_administrators"}}</a></div>{{end}}
{{end}}
</div>
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="#">{{lang "panel_menu_system"}}</a></div>
</div>
<div class="colstack_item rowmenu">
{{if .CurrentUser.Perms.ManagePlugins}}<div class="rowitem passive">
<a href="/panel/plugins/">{{lang "panel_menu_plugins"}}</a>
</div>{{end}}
{{if .CurrentUser.IsSuperAdmin}}<div class="rowitem passive">
<a href="/panel/backups/">{{lang "panel_menu_backups"}}</a>
</div>{{end}}
{{if .CurrentUser.IsAdmin}}<div class="rowitem passive">
<a href="/panel/debug/">{{lang "panel_menu_debug"}}</a>
</div>{{end}}
</div>

View File

@ -0,0 +1,5 @@
<nav class="colstack_left" aria-label="{{lang "panel_menu_aria"}}">
<div class="colstack_item colstack_head">
<div class="rowitem back_to_site"><a href="/panel/">Back to site</a></div>
</div>
{{template "panel_inner_menu.html" . }}</nav>

View File

@ -24,6 +24,9 @@
.menu_stats { .menu_stats {
margin-left: 4px; margin-left: 4px;
} }
.back_to_site {
font-size: 18px;
}
.colstack_right { .colstack_right {
background-color: #333333; background-color: #333333;