mirror of
https://github.com/writefreely/writefreely
synced 2024-11-10 11:24:13 +00:00
Add user management pages
This commit is contained in:
parent
ebeacff43c
commit
5e53a1788d
14 changed files with 1183 additions and 0 deletions
18
static/js/ace.js
Normal file
18
static/js/ace.js
Normal file
File diff suppressed because one or more lines are too long
9
static/js/mode-css.js
Normal file
9
static/js/mode-css.js
Normal file
File diff suppressed because one or more lines are too long
105
static/js/postactions.js
Normal file
105
static/js/postactions.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
var postActions = function() {
|
||||
var $container = He.get('moving');
|
||||
var MultiMove = function(el, id) {
|
||||
var lbl = el.options[el.selectedIndex].textContent;
|
||||
var collAlias = el.options[el.selectedIndex].value;
|
||||
var $lbl = He.$('label[for=move-'+id+']')[0];
|
||||
$lbl.textContent = "moving to "+lbl+"...";
|
||||
var params;
|
||||
if (collAlias == '|anonymous|') {
|
||||
params = [id];
|
||||
} else {
|
||||
params = [{
|
||||
id: id
|
||||
}];
|
||||
}
|
||||
var callback = function(code, resp) {
|
||||
if (code == 200) {
|
||||
for (var i=0; i<resp.data.length; i++) {
|
||||
if (resp.data[i].code == 200) {
|
||||
$lbl.innerHTML = "moved to <strong>"+lbl+"</strong>";
|
||||
var newPostURL = "/"+collAlias+"/"+resp.data[i].post.slug;
|
||||
try {
|
||||
// Posts page
|
||||
He.$('#post-'+resp.data[i].post.id+' > h3 > a')[0].href = newPostURL;
|
||||
} catch (e) {
|
||||
// Blog index
|
||||
var $article = He.get('post-'+resp.data[i].post.id);
|
||||
$article.className = 'norm moved';
|
||||
if (collAlias == '|anonymous|') {
|
||||
$article.innerHTML = '<p><a href="/'+resp.data[i].post.id+'">Unpublished post</a>.</p>';
|
||||
} else {
|
||||
$article.innerHTML = '<p>Moved to <a style="font-weight:bold" href="'+newPostURL+'">'+lbl+'</a>.</p>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$lbl.innerHTML = "unable to move: "+resp.data[i].error_msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (collAlias == '|anonymous|') {
|
||||
He.postJSON("/api/posts/disperse", params, callback);
|
||||
} else {
|
||||
He.postJSON("/api/collections/"+collAlias+"/collect", params, callback);
|
||||
}
|
||||
};
|
||||
var Move = function(el, id, collAlias) {
|
||||
var lbl = el.textContent;
|
||||
try {
|
||||
var m = lbl.match(/move to (.*)/);
|
||||
lbl = m[1];
|
||||
} catch (e) {
|
||||
if (collAlias == '|anonymous|') {
|
||||
lbl = "draft";
|
||||
}
|
||||
}
|
||||
|
||||
el.textContent = "moving to "+lbl+"...";
|
||||
if (collAlias == '|anonymous|') {
|
||||
params = [id];
|
||||
} else {
|
||||
params = [{
|
||||
id: id
|
||||
}];
|
||||
}
|
||||
var callback = function(code, resp) {
|
||||
if (code == 200) {
|
||||
for (var i=0; i<resp.data.length; i++) {
|
||||
if (resp.data[i].code == 200) {
|
||||
el.innerHTML = "moved to <strong>"+lbl+"</strong>";
|
||||
el.onclick = null;
|
||||
var newPostURL = "/"+collAlias+"/"+resp.data[i].post.slug;
|
||||
el.href = newPostURL;
|
||||
el.title = "View on "+lbl;
|
||||
try {
|
||||
// Posts page
|
||||
He.$('#post-'+resp.data[i].post.id+' > h3 > a')[0].href = newPostURL;
|
||||
} catch (e) {
|
||||
// Blog index
|
||||
var $article = He.get('post-'+resp.data[i].post.id);
|
||||
$article.className = 'norm moved';
|
||||
if (collAlias == '|anonymous|') {
|
||||
$article.innerHTML = '<p><a href="/'+resp.data[i].post.id+'">Unpublished post</a>.</p>';
|
||||
} else {
|
||||
$article.innerHTML = '<p>Moved to <a style="font-weight:bold" href="'+newPostURL+'">'+lbl+'</a>.</p>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
el.innerHTML = "unable to move: "+resp.data[i].error_msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (collAlias == '|anonymous|') {
|
||||
He.postJSON("/api/posts/disperse", params, callback);
|
||||
} else {
|
||||
He.postJSON("/api/collections/"+collAlias+"/collect", params, callback);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
move: Move,
|
||||
multiMove: MultiMove,
|
||||
};
|
||||
}();
|
315
static/js/posts.js
Normal file
315
static/js/posts.js
Normal file
|
@ -0,0 +1,315 @@
|
|||
/**
|
||||
* Functionality for managing local Write.as posts.
|
||||
*
|
||||
* Dependencies:
|
||||
* h.js
|
||||
*/
|
||||
function toggleTheme() {
|
||||
var btns;
|
||||
try {
|
||||
btns = Array.prototype.slice.call(document.getElementById('belt').querySelectorAll('.tool img'));
|
||||
} catch (e) {}
|
||||
if (document.body.className == 'light') {
|
||||
document.body.className = 'dark';
|
||||
try {
|
||||
for (var i=0; i<btns.length; i++) {
|
||||
btns[i].src = btns[i].src.replace('_dark@2x.png', '@2x.png');
|
||||
}
|
||||
} catch (e) {}
|
||||
} else if (document.body.className == 'dark') {
|
||||
document.body.className = 'light';
|
||||
try {
|
||||
for (var i=0; i<btns.length; i++) {
|
||||
btns[i].src = btns[i].src.replace('@2x.png', '_dark@2x.png');
|
||||
}
|
||||
} catch (e) {}
|
||||
} else {
|
||||
// Don't alter the theme
|
||||
return;
|
||||
}
|
||||
H.set('padTheme', document.body.className);
|
||||
}
|
||||
if (H.get('padTheme', 'light') != 'light') {
|
||||
toggleTheme();
|
||||
}
|
||||
|
||||
var deleting = false;
|
||||
function delPost(e, id, owned) {
|
||||
e.preventDefault();
|
||||
if (deleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: UNDO!
|
||||
if (window.confirm('Are you sure you want to delete this post?')) {
|
||||
var token;
|
||||
for (var i=0; i<posts.length; i++) {
|
||||
if (posts[i].id == id) {
|
||||
token = posts[i].token;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (owned || token) {
|
||||
// AJAX
|
||||
deletePost(id, token, function() {
|
||||
// Remove post from list
|
||||
var $postEl = document.getElementById('post-' + id);
|
||||
$postEl.parentNode.removeChild($postEl);
|
||||
|
||||
if (posts.length == 0) {
|
||||
displayNoPosts();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill in full page of posts
|
||||
var $postsChildren = $posts.el.getElementsByClassName('post');
|
||||
if ($postsChildren.length < postsPerPage && $postsChildren.length < posts.length) {
|
||||
var lastVisiblePostID = $postsChildren[$postsChildren.length-1].id;
|
||||
lastVisiblePostID = lastVisiblePostID.substr(lastVisiblePostID.indexOf('-')+1);
|
||||
|
||||
for (var i=0; i<posts.length-1; i++) {
|
||||
if (posts[i].id == lastVisiblePostID) {
|
||||
var $moreBtn = document.getElementById('more-posts');
|
||||
if ($moreBtn) {
|
||||
// Should always land here (?)
|
||||
$posts.el.insertBefore(createPostEl(posts[i-1]), $moreBtn);
|
||||
} else {
|
||||
$posts.el.appendChild(createPostEl(posts[i-1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert('Something went seriously wrong. Try refreshing.');
|
||||
}
|
||||
}
|
||||
}
|
||||
var getFormattedDate = function(d) {
|
||||
var mos = [
|
||||
"January", "February", "March",
|
||||
"April", "May", "June", "July",
|
||||
"August", "September", "October",
|
||||
"November", "December"
|
||||
];
|
||||
|
||||
var day = d.getDate();
|
||||
var mo = d.getMonth();
|
||||
var yr = d.getFullYear();
|
||||
return mos[mo] + ' ' + day + ', ' + yr;
|
||||
};
|
||||
var posts = JSON.parse(H.get('posts', '[]'));
|
||||
|
||||
var initialListPop = function() {
|
||||
pages = Math.ceil(posts.length / postsPerPage);
|
||||
|
||||
loadPage(page, true);
|
||||
};
|
||||
|
||||
var $posts = H.getEl("posts");
|
||||
if ($posts.el == null) {
|
||||
$posts = H.getEl("unsynced-posts");
|
||||
}
|
||||
$posts.el.innerHTML = '<p class="status">Reading...</p>';
|
||||
var createMorePostsEl = function() {
|
||||
var $more = document.createElement('div');
|
||||
var nextPage = page+1;
|
||||
$more.id = 'more-posts';
|
||||
$more.innerHTML = '<p><a href="#' + nextPage + '">More...</a></p>';
|
||||
|
||||
return $more;
|
||||
};
|
||||
|
||||
var localPosts = function() {
|
||||
var $delPost, lastDelPost, lastInfoHTML;
|
||||
var $info = He.get('unsynced-posts-info');
|
||||
|
||||
var findPostIdx = function(id) {
|
||||
for (var i=0; i<posts.length; i++) {
|
||||
if (posts[i].id == id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
var DismissError = function(e, el) {
|
||||
e.preventDefault();
|
||||
var $errorMsg = el.parentNode.previousElementSibling;
|
||||
$errorMsg.parentNode.removeChild($errorMsg);
|
||||
var $errorMsgNav = el.parentNode;
|
||||
$errorMsgNav.parentNode.removeChild($errorMsgNav);
|
||||
};
|
||||
var DeletePostLocal = function(e, el, id) {
|
||||
e.preventDefault();
|
||||
if (!window.confirm('Are you sure you want to delete this post?')) {
|
||||
return;
|
||||
}
|
||||
var i = findPostIdx(id);
|
||||
if (i > -1) {
|
||||
lastDelPost = posts.splice(i, 1)[0];
|
||||
$delPost = H.getEl('post-'+id);
|
||||
$delPost.setClass('del-undo');
|
||||
var $unsyncPosts = document.getElementById('unsynced-posts');
|
||||
var visible = $unsyncPosts.children.length;
|
||||
for (var i=0; i < $unsyncPosts.children.length; i++) { // NOTE: *.children support in IE9+
|
||||
if ($unsyncPosts.children[i].className.indexOf('del-undo') !== -1) {
|
||||
visible--;
|
||||
}
|
||||
}
|
||||
if (visible == 0) {
|
||||
H.getEl('unsynced-posts-header').hide();
|
||||
// TODO: fix undo functionality and don't do the following:
|
||||
H.getEl('unsynced-posts-info').hide();
|
||||
}
|
||||
H.set('posts', JSON.stringify(posts));
|
||||
// TODO: fix undo functionality and re-add
|
||||
//lastInfoHTML = $info.innerHTML;
|
||||
//$info.innerHTML = 'Unsynced entry deleted. <a href="#" onclick="localPosts.undoDelete()">Undo</a>.';
|
||||
}
|
||||
};
|
||||
var UndoDelete = function() {
|
||||
// TODO: fix this header reappearing
|
||||
H.getEl('unsynced-posts-header').show();
|
||||
$delPost.removeClass('del-undo');
|
||||
$info.innerHTML = lastInfoHTML;
|
||||
};
|
||||
|
||||
return {
|
||||
dismissError: DismissError,
|
||||
deletePost: DeletePostLocal,
|
||||
undoDelete: UndoDelete,
|
||||
};
|
||||
}();
|
||||
var createPostEl = function(post) {
|
||||
var $post = document.createElement('div');
|
||||
var title = (post.title || post.id);
|
||||
title = title.replace(/</g, "<");
|
||||
$post.id = 'post-' + post.id;
|
||||
$post.className = 'post';
|
||||
$post.innerHTML = '<h3><a href="/' + post.id + '">' + title + '</a></h3>';
|
||||
|
||||
var posted = "";
|
||||
if (post.created) {
|
||||
posted = getFormattedDate(new Date(post.created))
|
||||
}
|
||||
var hasDraft = H.exists('draft' + post.id);
|
||||
$post.innerHTML += '<h4><date>' + posted + '</date> <a class="action" href="/pad/' + post.id + '">edit' + (hasDraft ? 'ed' : '') + '</a> <a class="delete action" href="/' + post.id + '" onclick="delPost(event, \'' + post.id + '\')">delete</a></h4>';
|
||||
|
||||
if (post.error) {
|
||||
$post.innerHTML += '<p class="error"><strong>Sync error:</strong> ' + post.error + ' <nav><a href="#" onclick="localPosts.dismissError(event, this)">dismiss</a> <a href="#" onclick="localPosts.deletePost(event, this, \''+post.id+'\')">remove post</a></nav></p>';
|
||||
}
|
||||
if (post.summary) {
|
||||
$post.innerHTML += '<p>' + post.summary.replace(/</g, "<") + '</p>';
|
||||
}
|
||||
return $post;
|
||||
};
|
||||
var loadPage = function(p, loadAll) {
|
||||
if (loadAll) {
|
||||
$posts.el.innerHTML = '';
|
||||
}
|
||||
|
||||
var startPost = posts.length - 1 - (loadAll ? 0 : ((p-1)*postsPerPage));
|
||||
var endPost = posts.length - 1 - (p*postsPerPage);
|
||||
for (var i=startPost; i>=0 && i>endPost; i--) {
|
||||
$posts.el.appendChild(createPostEl(posts[i]));
|
||||
}
|
||||
|
||||
if (loadAll) {
|
||||
if (p < pages) {
|
||||
$posts.el.appendChild(createMorePostsEl());
|
||||
}
|
||||
} else {
|
||||
var $moreEl = document.getElementById('more-posts');
|
||||
$moreEl.parentNode.removeChild($moreEl);
|
||||
}
|
||||
try {
|
||||
postsLoaded(posts.length);
|
||||
} catch (e) {}
|
||||
};
|
||||
var getPageNum = function(url) {
|
||||
var hash;
|
||||
if (url) {
|
||||
hash = url.substr(url.indexOf('#')+1);
|
||||
} else {
|
||||
hash = window.location.hash.substr(1);
|
||||
}
|
||||
|
||||
var page = hash || 1;
|
||||
page = parseInt(page);
|
||||
if (isNaN(page)) {
|
||||
page = 1;
|
||||
}
|
||||
|
||||
return page;
|
||||
};
|
||||
|
||||
var postsPerPage = 10;
|
||||
var pages = 0;
|
||||
var page = getPageNum();
|
||||
|
||||
window.addEventListener('hashchange', function(e) {
|
||||
var newPage = getPageNum();
|
||||
var didPageIncrement = newPage == getPageNum(e.oldURL) + 1;
|
||||
|
||||
loadPage(newPage, !didPageIncrement);
|
||||
});
|
||||
|
||||
var deletePost = function(postID, token, callback) {
|
||||
deleting = true;
|
||||
|
||||
var $delBtn = document.getElementById('post-' + postID).getElementsByClassName('delete action')[0];
|
||||
$delBtn.innerHTML = '...';
|
||||
|
||||
var http = new XMLHttpRequest();
|
||||
var url = "/api/posts/" + postID + (typeof token !== 'undefined' ? "?token=" + encodeURIComponent(token) : '');
|
||||
http.open("DELETE", url, true);
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
deleting = false;
|
||||
if (http.status == 204 || http.status == 404) {
|
||||
for (var i=0; i<posts.length; i++) {
|
||||
if (posts[i].id == postID) {
|
||||
// TODO: use this return value, along will full content, for restoring post
|
||||
posts.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
H.set('posts', JSON.stringify(posts));
|
||||
|
||||
callback();
|
||||
} else if (http.status == 409) {
|
||||
$delBtn.innerHTML = 'delete';
|
||||
alert("Post is synced to another account. Delete the post from that account instead.");
|
||||
// TODO: show "remove" button instead of "delete" now
|
||||
// Persist that state.
|
||||
// Have it remove the post locally only.
|
||||
} else {
|
||||
$delBtn.innerHTML = 'delete';
|
||||
alert("Failed to delete. Please try again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
http.send();
|
||||
};
|
||||
|
||||
var hasWritten = H.get('lastDoc', '') !== '';
|
||||
|
||||
var displayNoPosts = function() {
|
||||
if (auth) {
|
||||
$posts.el.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
var cta = '<a href="/pad">Create a post</a> and it\'ll appear here.';
|
||||
if (hasWritten) {
|
||||
cta = '<a href="/pad">Finish your post</a> and it\'ll appear here.';
|
||||
}
|
||||
H.getEl("posts").el.innerHTML = '<p class="status">No posts created yet.</p><p class="status">' + cta + '</p>';
|
||||
};
|
||||
|
||||
if (posts.length == 0) {
|
||||
displayNoPosts();
|
||||
} else {
|
||||
initialListPop();
|
||||
}
|
||||
|
9
static/js/theme-chrome.js
Normal file
9
static/js/theme-chrome.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
ace.define("ace/theme/chrome",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-chrome",t.cssText='.ace-chrome .ace_gutter {background: #ebebeb;color: #333;overflow : hidden;}.ace-chrome .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-chrome {background-color: #FFFFFF;color: black;}.ace-chrome .ace_cursor {color: black;}.ace-chrome .ace_invisible {color: rgb(191, 191, 191);}.ace-chrome .ace_constant.ace_buildin {color: rgb(88, 72, 246);}.ace-chrome .ace_constant.ace_language {color: rgb(88, 92, 246);}.ace-chrome .ace_constant.ace_library {color: rgb(6, 150, 14);}.ace-chrome .ace_invalid {background-color: rgb(153, 0, 0);color: white;}.ace-chrome .ace_fold {}.ace-chrome .ace_support.ace_function {color: rgb(60, 76, 114);}.ace-chrome .ace_support.ace_constant {color: rgb(6, 150, 14);}.ace-chrome .ace_support.ace_type,.ace-chrome .ace_support.ace_class.ace-chrome .ace_support.ace_other {color: rgb(109, 121, 222);}.ace-chrome .ace_variable.ace_parameter {font-style:italic;color:#FD971F;}.ace-chrome .ace_keyword.ace_operator {color: rgb(104, 118, 135);}.ace-chrome .ace_comment {color: #236e24;}.ace-chrome .ace_comment.ace_doc {color: #236e24;}.ace-chrome .ace_comment.ace_doc.ace_tag {color: #236e24;}.ace-chrome .ace_constant.ace_numeric {color: rgb(0, 0, 205);}.ace-chrome .ace_variable {color: rgb(49, 132, 149);}.ace-chrome .ace_xml-pe {color: rgb(104, 104, 91);}.ace-chrome .ace_entity.ace_name.ace_function {color: #0000A2;}.ace-chrome .ace_heading {color: rgb(12, 7, 255);}.ace-chrome .ace_list {color:rgb(185, 6, 144);}.ace-chrome .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-chrome .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-chrome .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-chrome .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-chrome .ace_marker-layer .ace_active-line {background: rgba(0, 0, 0, 0.07);}.ace-chrome .ace_gutter-active-line {background-color : #dcdcdc;}.ace-chrome .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-chrome .ace_storage,.ace-chrome .ace_keyword,.ace-chrome .ace_meta.ace_tag {color: rgb(147, 15, 128);}.ace-chrome .ace_string.ace_regex {color: rgb(255, 0, 0)}.ace-chrome .ace_string {color: #1A1AA6;}.ace-chrome .ace_entity.ace_other.ace_attribute-name {color: #994409;}.ace-chrome .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}';var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)});
|
||||
(function() {
|
||||
ace.require(["ace/theme/chrome"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
1
static/js/worker-css.js
Normal file
1
static/js/worker-css.js
Normal file
File diff suppressed because one or more lines are too long
145
templates/user/articles.tmpl
Normal file
145
templates/user/articles.tmpl
Normal file
|
@ -0,0 +1,145 @@
|
|||
{{define "articles"}}
|
||||
{{template "header" .}}
|
||||
|
||||
<div class="snug content-container">
|
||||
|
||||
{{if .Flashes}}<ul class="errors">
|
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||
</ul>{{end}}
|
||||
|
||||
<h2 id="posts-header">drafts</h2>
|
||||
|
||||
{{ if .AnonymousPosts }}<div class="atoms posts">
|
||||
{{ range $el := .AnonymousPosts }}<div id="post-{{.ID}}" class="post">
|
||||
<h3><a href="/{{.ID}}" itemprop="url">{{.DisplayTitle}}</a></h3>
|
||||
<h4>
|
||||
<date datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</date>
|
||||
<a class="action" href="/{{.ID}}/edit">edit</a>
|
||||
<a class="delete action" href="/{{.ID}}" onclick="delPost(event, '{{.ID}}', true)">delete</a>
|
||||
{{ if $.Collections }}
|
||||
{{if gt (len $.Collections) 1}}<div class="action flat-select">
|
||||
<select id="move-{{.ID}}" onchange="postActions.multiMove(this, '{{.ID}}')" title="Move this post to one of your blogs">
|
||||
<option style="display:none"></option>
|
||||
{{range $.Collections}}<option value="{{.Alias}}">{{.DisplayTitle}}</option>{{end}}
|
||||
</select>
|
||||
<label for="move-{{.ID}}">move to...</label>
|
||||
<img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" />
|
||||
</div>{{else}}
|
||||
{{range $.Collections}}
|
||||
<a class="action" href="/{{$el.ID}}" title="Publish this post to your blog '{{.DisplayTitle}}'" onclick="postActions.move(this, '{{$el.ID}}', '{{.Alias}}');return false">move to {{.DisplayTitle}}</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{ end }}
|
||||
</h4>
|
||||
{{if .Summary}}<p>{{.Summary}}</p>{{end}}
|
||||
</div>{{end}}
|
||||
</div>{{ else }}<div id="no-posts-published"><p>You haven't saved any drafts yet.</p>
|
||||
<p>They'll show up here once you do. Find your blog posts from the <a href="/me/c/">Blogs</a> page.</p>
|
||||
<p class="text-cta"><a href="/">Start writing</a></p></div>{{ end }}
|
||||
|
||||
<div id="moving"></div>
|
||||
|
||||
<h2 id="unsynced-posts-header" style="display: none">unsynced posts</h2>
|
||||
<div id="unsynced-posts-info" style="margin-top: 1em"></div>
|
||||
<div id="unsynced-posts" class="atoms"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/h.js"></script>
|
||||
<script src="/js/postactions.js"></script>
|
||||
<script>
|
||||
var auth = true;
|
||||
function postsLoaded(n) {
|
||||
if (n == 0) {
|
||||
return;
|
||||
}
|
||||
document.getElementById('unsynced-posts-header').style.display = 'block';
|
||||
var syncing = false;
|
||||
var $pInfo = document.getElementById('unsynced-posts-info');
|
||||
$pInfo.className = 'alert info';
|
||||
var plural = n != 1;
|
||||
$pInfo.innerHTML = '<p>You have <strong>'+n+'</strong> post'+(plural?'s that aren\'t':' that isn\'t')+' synced to your account yet. <a href="#" id="btn-sync">Sync '+(plural?'them':'it')+' now</a>.</p>';
|
||||
|
||||
var $noPosts = document.getElementById('no-posts-published');
|
||||
if ($noPosts != null) {
|
||||
$noPosts.style.display = 'none';
|
||||
document.getElementById('posts-header').style.display = 'none';
|
||||
}
|
||||
|
||||
H.getEl('btn-sync').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
if (syncing) {
|
||||
return;
|
||||
}
|
||||
var http = new XMLHttpRequest();
|
||||
var params = [];
|
||||
var posts = JSON.parse(H.get('posts', '[]'));
|
||||
if (posts.length > 0) {
|
||||
for (var i=0; i<posts.length; i++) {
|
||||
params.push({id: posts[i].id, token: posts[i].token});
|
||||
}
|
||||
}
|
||||
|
||||
this.style.fontWeight = 'bold';
|
||||
this.innerText = 'Syncing '+(plural?'them':'it')+' now...';
|
||||
|
||||
http.open("POST", "/api/posts/claim", true);
|
||||
|
||||
// Send the proper header information along with the request
|
||||
http.setRequestHeader("Content-type", "application/json");
|
||||
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
syncing = false;
|
||||
this.innerText = 'Importing '+(plural?'them':'it')+' now...';
|
||||
|
||||
if (http.status == 200) {
|
||||
var res = JSON.parse(http.responseText);
|
||||
if (res.data.length > 0) {
|
||||
if (res.data.length != posts.length) {
|
||||
// TODO: handle something that royally fucked up
|
||||
console.error("Request and result array length didn't match!");
|
||||
return;
|
||||
}
|
||||
for (var i=0; i<res.data.length; i++) {
|
||||
if (res.data[i].code == 200) {
|
||||
// Post successfully claimed.
|
||||
for (var j=0; j<posts.length; j++) {
|
||||
// Find post in local store
|
||||
if (posts[j].id == res.data[i].post.id) {
|
||||
// Remove this post
|
||||
posts.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var j=0; j<posts.length; j++) {
|
||||
// Find post in local store
|
||||
if (posts[j].id == res.data[i].id) {
|
||||
// Note the error in the local post
|
||||
posts[j].error = res.data[i].error_msg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
H.set('posts', JSON.stringify(posts));
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
// TODO: handle error visually (option to retry)
|
||||
console.error("Didn't work at all, man.");
|
||||
this.style.fontWeight = 'normal';
|
||||
this.innerText = 'Sync '+(plural?'them':'it')+' now';
|
||||
}
|
||||
}
|
||||
}
|
||||
http.send(JSON.stringify(params));
|
||||
syncing = true;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="/js/posts.js"></script>
|
||||
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
234
templates/user/collection.tmpl
Normal file
234
templates/user/collection.tmpl
Normal file
|
@ -0,0 +1,234 @@
|
|||
{{define "upgrade"}}
|
||||
<p><a href="/me/plan?to=/me/c/{{.Alias}}">Upgrade</a> for <span>$40 / year</span> to edit.</p>
|
||||
{{end}}
|
||||
|
||||
{{define "collection"}}
|
||||
{{template "header" .}}
|
||||
|
||||
<div class="content-container snug">
|
||||
<div id="overlay"></div>
|
||||
|
||||
<h2>Customize {{.DisplayTitle}} <a href="/{{.Alias}}/">view blog</a></h2>
|
||||
|
||||
{{if .Flashes}}<ul class="errors">
|
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||
</ul>{{end}}
|
||||
|
||||
<form name="customize-form" action="/api/collections/{{.Alias}}" method="post" onsubmit="return disableSubmit()">
|
||||
<div id="collection-options">
|
||||
<div style="text-align:center">
|
||||
<h1><input type="text" name="title" id="title" value="{{.DisplayTitle}}" placeholder="Title" /></h1>
|
||||
<p><input type="text" name="description" id="description" value="{{.Description}}" placeholder="Description" /></p>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<h2><a name="preferred-url"></a>URL</h2>
|
||||
<div class="section">
|
||||
{{if eq .Alias .Username}}<p style="font-size: 0.8em">This blog uses your username in its URL{{if .Federation}} and fediverse handle{{end}}. You can change it in your <a href="/me/settings">Account Settings</a>.</p>{{end}}
|
||||
<ul style="list-style:none">
|
||||
<li>
|
||||
{{.FriendlyHost}}/<strong>{{.Alias}}</strong>/
|
||||
</li>
|
||||
<li>
|
||||
<strong id="normal-handle-env" class="fedi-handle" {{if or (not .Federation) .SingleUser}}style="display:none"{{end}}>@<span id="fedi-handle">{{.Alias}}</span>@<span id="fedi-domain">{{.FriendlyHost}}</span></strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<h2>Publicity</h2>
|
||||
<div class="section">
|
||||
<ul style="list-style:none">
|
||||
<li>
|
||||
<label><input type="radio" name="visibility" id="visibility-unlisted" value="0" {{if .IsUnlisted}}checked="checked"{{end}} />
|
||||
Unlisted
|
||||
</label>
|
||||
<p>This blog is visible to anyone with its link.</p>
|
||||
</li>
|
||||
<li>
|
||||
<label class="option-text"><input type="radio" name="visibility" id="visibility-private" value="2" {{if .IsPrivate}}checked="checked"{{end}} />
|
||||
Private
|
||||
</label>
|
||||
<p>Only you may read this blog (while you're logged in).</p>
|
||||
</li>
|
||||
<li>
|
||||
<label class="option-text"><input type="radio" name="visibility" id="visibility-protected" value="4" {{if .IsProtected}}checked="checked"{{end}} />
|
||||
Password-protected: <input type="password" class="low-profile" name="password" id="collection-pass" autocomplete="new-password" placeholder="{{if .IsProtected}}xxxxxxxxxxxxxxxx{{else}}a memorable password{{end}}" />
|
||||
</label>
|
||||
<p>A password is required to read this blog.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<h2>Display Format</h2>
|
||||
<div class="section">
|
||||
<p class="explain">Customize how your posts display on your page.
|
||||
</p>
|
||||
<ul style="list-style:none">
|
||||
<li>
|
||||
<label><input type="radio" name="format" id="format-blog" value="blog" {{if or (not .Format) (eq .Format "blog")}}checked="checked"{{end}} />
|
||||
Blog
|
||||
</label>
|
||||
<p>Dates are shown. Latest posts listed first.</p>
|
||||
</li>
|
||||
<li>
|
||||
<label class="option-text"><input type="radio" name="format" id="format-novel" value="novel" {{if eq .Format "novel"}}checked="checked"{{end}} />
|
||||
Novel
|
||||
</label>
|
||||
<p>No dates shown. Oldest posts first.</p>
|
||||
</li>
|
||||
<li>
|
||||
<label class="option-text"><input type="radio" name="format" id="format-notebook" value="notebook" {{if eq .Format "notebook"}}checked="checked"{{end}} />
|
||||
Notebook
|
||||
</label>
|
||||
<p>No dates shown. Latest posts first.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<h2>Text Rendering</h2>
|
||||
<div class="section">
|
||||
<p class="explain">Customize how plain text renders on your blog.</p>
|
||||
<ul style="list-style:none">
|
||||
<li>
|
||||
<label class="option-text disabled"><input type="checkbox" name="markdown" checked="checked" disabled />
|
||||
Markdown
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label><input type="checkbox" name="mathjax" {{if .RenderMathJax}}checked="checked"{{end}} />
|
||||
MathJax
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<h2>Custom CSS</h2>
|
||||
<div class="section">
|
||||
<textarea id="css-editor" class="section codable" name="style_sheet">{{.StyleSheet}}</textarea>
|
||||
<p class="explain">See our guide on <a href="https://guides.write.as/customizing/#custom-css">customization</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option" style="text-align: center; margin-top: 4em;">
|
||||
<input type="submit" id="save-changes" value="Save changes" />
|
||||
<p><a href="/{{.Alias}}/">View Blog</a></p>
|
||||
{{if ne .Alias .Username}}<p><a class="danger" href="#modal-delete" onclick="promptDelete();">Delete Blog...</a></p>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="modal-delete" class="modal">
|
||||
<h2>Are you sure you want to delete this blog?</h2>
|
||||
<div class="body short">
|
||||
<p style="text-align:left">This will permanently erase <strong>{{.DisplayTitle}}</strong> ({{.FriendlyHost}}/{{.Alias}}) from the internet. Any posts on this blog will be saved and made into drafts (found on your <a href="/me/posts/">Drafts</a> page).</p>
|
||||
<p>If you're sure you want to delete this blog, enter its name in the box below and press <strong>Delete</strong>.</p>
|
||||
|
||||
<ul id="delete-errors" class="errors"></ul>
|
||||
|
||||
<input id="confirm-text" placeholder="{{.Alias}}" type="text" class="boxy" style="margin-top: 0.5em;" />
|
||||
<div style="text-align:right; margin-top: 1em;">
|
||||
<a id="cancel-delete" style="margin-right:2em" href="#">Cancel</a>
|
||||
<button id="btn-delete" class="danger" onclick="deleteBlog(); return false;">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/h.js"></script>
|
||||
<script src="/js/ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script>
|
||||
// Begin shared modal code
|
||||
function showModal(id) {
|
||||
document.getElementById('overlay').style.display = 'block';
|
||||
document.getElementById('modal-'+id).style.display = 'block';
|
||||
}
|
||||
|
||||
var closeModals = function(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('overlay').style.display = 'none';
|
||||
var modals = document.querySelectorAll('.modal');
|
||||
for (var i=0; i<modals.length; i++) {
|
||||
modals[i].style.display = 'none';
|
||||
}
|
||||
};
|
||||
H.getEl('overlay').on('click', closeModals);
|
||||
H.getEl('cancel-delete').on('click', closeModals);
|
||||
// end
|
||||
var deleteBlog = function(e) {
|
||||
if (document.getElementById('confirm-text').value != '{{.Alias}}') {
|
||||
document.getElementById('delete-errors').innerHTML = '<li class="urgent">Enter <strong>{{.Alias}}</strong> in the box below.</li>';
|
||||
return;
|
||||
}
|
||||
// Clear errors
|
||||
document.getElementById('delete-errors').innerHTML = '';
|
||||
document.getElementById('btn-delete').innerHTML = 'Deleting...';
|
||||
|
||||
var http = new XMLHttpRequest();
|
||||
var url = "/api/collections/{{.Alias}}?web=1";
|
||||
http.open("DELETE", url, true);
|
||||
http.setRequestHeader("Content-type", "application/json");
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
if (http.status == 204) {
|
||||
window.location = '/me/c/';
|
||||
} else {
|
||||
var data = JSON.parse(http.responseText);
|
||||
document.getElementById('delete-errors').innerHTML = '<li class="urgent">'+data.error_msg+'</li>';
|
||||
document.getElementById('btn-delete').innerHTML = 'Delete';
|
||||
}
|
||||
}
|
||||
};
|
||||
http.send(null);
|
||||
};
|
||||
|
||||
function createHidden(theForm, key, value) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = key;
|
||||
input.value = value;
|
||||
theForm.appendChild(input);
|
||||
}
|
||||
function disableSubmit() {
|
||||
var $form = document.forms['customize-form'];
|
||||
createHidden($form, 'style_sheet', cssEditor.getSession().getValue());
|
||||
createHidden($form, 'script', jsEditor.getSession().getValue());
|
||||
var $btn = document.getElementById("save-changes");
|
||||
$btn.value = "Saving changes...";
|
||||
$btn.disabled = true;
|
||||
return true;
|
||||
}
|
||||
function promptDelete() {
|
||||
showModal("delete");
|
||||
}
|
||||
|
||||
var $fediDomain = document.getElementById('fedi-domain');
|
||||
var $fediCustomDomain = document.getElementById('fedi-custom-domain');
|
||||
var $customDomain = document.getElementById('domain-alias');
|
||||
var $customHandleEnv = document.getElementById('custom-handle-env');
|
||||
var $normalHandleEnv = document.getElementById('normal-handle-env');
|
||||
|
||||
var opt = {
|
||||
showLineNumbers: false,
|
||||
showPrintMargin: 0,
|
||||
};
|
||||
var theme = "ace/theme/chrome";
|
||||
var cssEditor = ace.edit("css-editor");
|
||||
cssEditor.setTheme(theme);
|
||||
cssEditor.session.setMode("ace/mode/css");
|
||||
cssEditor.setOptions(opt);
|
||||
var jsEditor = ace.edit("js-editor");
|
||||
jsEditor.setTheme(theme);
|
||||
jsEditor.session.setMode("ace/mode/javascript");
|
||||
jsEditor.setOptions(opt);
|
||||
</script>
|
||||
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
111
templates/user/collections.tmpl
Normal file
111
templates/user/collections.tmpl
Normal file
|
@ -0,0 +1,111 @@
|
|||
{{define "collections"}}
|
||||
{{template "header" .}}
|
||||
|
||||
<div class="snug content-container">
|
||||
|
||||
{{if .Flashes}}<ul class="errors">
|
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||
</ul>{{end}}
|
||||
|
||||
<h2>blogs</h2>
|
||||
<ul class="atoms collections">
|
||||
{{range $i, $el := .Collections}}<li class="collection"><h3>
|
||||
<a class="title" href="/{{.Alias}}/">{{if .Title}}{{.Title}}{{else}}{{.Alias}}{{end}}</a>
|
||||
</h3>
|
||||
<h4>
|
||||
<a class="action new-post" href="/#{{.Alias}}">new post</a>
|
||||
<a class="action" href="/me/c/{{.Alias}}">customize</a>
|
||||
<a class="action" href="/me/c/{{.Alias}}/stats">stats</a>
|
||||
</h4>
|
||||
{{if .Description}}<p class="description">{{.Description}}</p>{{end}}
|
||||
</li>{{end}}
|
||||
<li id="create-collection">
|
||||
{{if not .NewBlogsDisabled}}
|
||||
<form method="POST" action="/api/collections" id="new-collection-form" onsubmit="return createCollection()">
|
||||
<h4>
|
||||
<input type="text" name="title" placeholder="Blog name" id="blog-name">
|
||||
<input type="hidden" name="web" value="true" />
|
||||
<input type="submit" value="Create" id="create-collection-btn">
|
||||
</h4>
|
||||
</form>
|
||||
{{end}}
|
||||
</li>
|
||||
</ul>
|
||||
{{if not .NewBlogsDisabled}}<p style="margin-top:0"><a id="new-collection" href="#new-collection">New blog</a></p>{{end}}
|
||||
|
||||
</div>
|
||||
|
||||
{{template "foot" .}}
|
||||
|
||||
<script src="/js/h.js"></script>
|
||||
<script>
|
||||
function createCollection() {
|
||||
var input = He.get('blog-name');
|
||||
if (input.value == "") {
|
||||
return false;
|
||||
}
|
||||
var form = He.get('new-collection-form');
|
||||
var submit = He.get('create-collection-btn');
|
||||
submit.value = "Creating...";
|
||||
submit.disabled = "disabled";
|
||||
He.postJSON("/api/collections", {
|
||||
title: input.value,
|
||||
web: true
|
||||
}, function(code, data) {
|
||||
if (data.code == 201) {
|
||||
location.reload();
|
||||
} else {
|
||||
var $createColl = document.getElementById('create-collection');
|
||||
var $submit = $createColl.querySelector('input[type=submit]');
|
||||
$submit.value = "Create";
|
||||
$submit.disabled = "";
|
||||
var $err = $createColl.querySelector('.error');
|
||||
if (data.code == 409) {
|
||||
if ($err === null) {
|
||||
var url = He.create('span');
|
||||
url.className = "error";
|
||||
url.innerText = "This name is taken.";
|
||||
$createColl.appendChild(url);
|
||||
} else {
|
||||
$err.innerText = "This name is taken.";
|
||||
}
|
||||
} else {
|
||||
if ($err === null) {
|
||||
var url = He.create('span');
|
||||
url.className = "error";
|
||||
url.innerText = data.error_msg;
|
||||
$createColl.appendChild(url);
|
||||
} else {
|
||||
$err.innerText = "This name is taken.";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
};
|
||||
(function() {
|
||||
H.getEl('new-collection').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var collForm = He.get('create-collection');
|
||||
if (collForm.style.display == '' || collForm.style.display == 'none') {
|
||||
// Show form
|
||||
this.textContent = "Cancel";
|
||||
collForm.style.display = 'list-item';
|
||||
collForm.querySelector('input[type=text]').focus();
|
||||
return;
|
||||
}
|
||||
// Hide form
|
||||
this.textContent = "New blog";
|
||||
collForm.style.display = 'none';
|
||||
});
|
||||
if (location.hash == '#new-collection' || location.hash == '#new') {
|
||||
var collForm = He.get('create-collection');
|
||||
collForm.style.display = 'list-item';
|
||||
collForm.querySelector('input[type=text]').focus();
|
||||
He.get('new-collection').textContent = "Cancel";
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
|
||||
{{template "body-end" .}}
|
||||
{{end}}
|
28
templates/user/export.tmpl
Normal file
28
templates/user/export.tmpl
Normal file
|
@ -0,0 +1,28 @@
|
|||
{{define "export"}}
|
||||
{{template "header" .}}
|
||||
|
||||
<div class="snug content-container">
|
||||
<h2 id="posts-header">Export</h2>
|
||||
<p>Your data on {{.SiteName}} is always free. Download and back-up your work any time.</p>
|
||||
|
||||
<table class="classy export">
|
||||
<tr>
|
||||
<th style="width: 40%">Export</th>
|
||||
<th colspan="2">Format</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Posts</th>
|
||||
<td><p class="text-cta"><a href="/me/posts/export.csv">CSV</a></p></td>
|
||||
<td><p class="text-cta"><a href="/me/posts/export.zip">ZIP</a></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>User + Blogs + Posts</th>
|
||||
<td><p class="text-cta"><a href="/me/export.json">JSON</a></p></td>
|
||||
<td><p class="text-cta"><a href="/me/export.json?pretty=1">Prettified</a></p></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
35
templates/user/include/footer.tmpl
Normal file
35
templates/user/include/footer.tmpl
Normal file
|
@ -0,0 +1,35 @@
|
|||
{{define "footer"}}
|
||||
{{template "foot" .}}
|
||||
{{template "body-end" .}}
|
||||
{{end}}
|
||||
{{define "foot"}}
|
||||
</div>
|
||||
<footer>
|
||||
<hr />
|
||||
<nav>
|
||||
<a class="home" href="/">{{.SiteName}}</a>
|
||||
<a href="/about">about</a>
|
||||
<a href="https://writefreely.org/guide" target="guides">guide</a>
|
||||
<a href="/privacy">privacy</a>
|
||||
<a>{{.Version}}</a>
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
<script type="text/javascript">
|
||||
try { // Google Fonts
|
||||
WebFontConfig = {
|
||||
custom: { families: [ 'Lora:400,700:latin' ], urls: [ '/css/fonts.css' ] }
|
||||
};
|
||||
(function() {
|
||||
var wf = document.createElement('script');
|
||||
wf.src = '/js/webfont.js';
|
||||
wf.type = 'text/javascript';
|
||||
wf.async = 'true';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(wf, s);
|
||||
})();
|
||||
} catch (e) { /* ¯\_(ツ)_/¯ */ }
|
||||
</script>
|
||||
{{end}}
|
||||
{{define "body-end"}}</body>
|
||||
</html>{{end}}
|
37
templates/user/include/header.tmpl
Normal file
37
templates/user/include/header.tmpl
Normal file
|
@ -0,0 +1,37 @@
|
|||
{{define "header"}}<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>{{.PageTitle}} {{if .Separator}}{{.Separator}}{{else}}—{{end}} {{.SiteName}}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/write.css" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#888888" />
|
||||
<meta name="apple-mobile-web-app-title" content="{{.SiteName}}">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/img/touch-icon-152.png">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/img/touch-icon-167.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/touch-icon-180.png">
|
||||
</head>
|
||||
<body id="me">
|
||||
<header>
|
||||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1>
|
||||
<nav id="user-nav">
|
||||
<nav class="dropdown-nav">
|
||||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
||||
<li><a href="/me/settings">Account settings</a></li>
|
||||
<li><a href="/me/export">Export</a></li>
|
||||
<li class="separator"><hr /></li>
|
||||
<li><a href="/me/logout">Log out</a></li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<nav class="tabs">
|
||||
<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>
|
||||
<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>
|
||||
</nav>
|
||||
</nav>
|
||||
</header>
|
||||
<div id="official-writing">
|
||||
{{end}}
|
83
templates/user/settings.tmpl
Normal file
83
templates/user/settings.tmpl
Normal file
|
@ -0,0 +1,83 @@
|
|||
{{define "settings"}}
|
||||
{{template "header" .}}
|
||||
|
||||
<style type="text/css">
|
||||
.option { margin: 2em 0em; }
|
||||
h3 { font-weight: normal; }
|
||||
.section > *:not(input) { font-size: 0.86em; }
|
||||
</style>
|
||||
<div class="content-container snug regular">
|
||||
<h2>{{if .IsLogOut}}Before you go...{{else}}Account Settings{{end}}</h2>
|
||||
{{if .Flashes}}<ul class="errors">
|
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||
</ul>{{end}}
|
||||
|
||||
{{ if .IsLogOut }}
|
||||
<div class="alert info">
|
||||
<p class="introduction">Please add an <strong>email address</strong> and/or <strong>passphrase</strong> so you can log in again later.</p>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="option">
|
||||
<p>Change your account settings here.</p>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/api/me/self" autocomplete="false">
|
||||
<div class="option">
|
||||
<h3>Username</h3>
|
||||
<div class="section">
|
||||
<input type="text" name="username" value="{{.Username}}" tabindex="1" />
|
||||
<input type="submit" value="Update" style="margin-left: 1em;" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
<form method="post" action="/api/me/self" autocomplete="false">
|
||||
<input type="hidden" name="logout" value="{{.IsLogOut}}" />
|
||||
<div class="option">
|
||||
<h3>Passphrase</h3>
|
||||
<div class="section">
|
||||
{{if and (not .HasPass) (not .IsLogOut)}}<div class="alert info"><p>Add a passphrase to easily log in to your account.</p></div>{{end}}
|
||||
{{if .HasPass}}<p>Current passphrase</p>
|
||||
<input type="password" name="current-pass" placeholder="Current passphrase" tabindex="1" /> <input class="show" type="checkbox" id="show-cur-pass" /><label for="show-cur-pass"> Show</label>
|
||||
<p>New passphrase</p>
|
||||
{{end}}
|
||||
{{if .IsLogOut}}<input type="text" value="{{.Username}}" style="display:none" />{{end}}
|
||||
<input type="password" name="new-pass" autocomplete="new-password" placeholder="New passphrase" tabindex="{{if .IsLogOut}}1{{else}}2{{end}}" /> <input class="show" type="checkbox" id="show-new-pass" /><label for="show-new-pass"> Show</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<h3>Email</h3>
|
||||
<div class="section">
|
||||
{{if and (not .Email) (not .IsLogOut)}}<div class="alert info"><p>Add your email to get:</p>
|
||||
<ul>
|
||||
<li>No-passphrase login</li>
|
||||
<li>Account recovery if you forget your passphrase</li>
|
||||
</ul></div>{{end}}
|
||||
<input type="email" name="email" style="letter-spacing: 1px" placeholder="Email address" value="{{.Email}}" size="40" tabindex="{{if .IsLogOut}}2{{else}}3{{end}}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option" style="text-align: center; margin-top: 4em;">
|
||||
<input type="submit" value="Save changes" tabindex="4" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var showChecks = document.querySelectorAll('input.show');
|
||||
for (var i=0; i<showChecks.length; i++) {
|
||||
showChecks[i].addEventListener('click', function() {
|
||||
var prevEl = this.previousElementSibling;
|
||||
if (prevEl.type == "password") {
|
||||
prevEl.type = "text";
|
||||
} else {
|
||||
prevEl.type = "password";
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
53
templates/user/stats.tmpl
Normal file
53
templates/user/stats.tmpl
Normal file
|
@ -0,0 +1,53 @@
|
|||
{{define "stats"}}
|
||||
{{template "header" .}}
|
||||
<style>
|
||||
table.classy th { text-align: left }
|
||||
table.classy th.num { text-align: right }
|
||||
td + td {
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
td.num {
|
||||
text-align: right;
|
||||
}
|
||||
table.classy.export a { text-transform: inherit; }
|
||||
td.none {
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="content-container snug">
|
||||
<h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2>
|
||||
|
||||
<p>Stats for all time.</p>
|
||||
|
||||
{{if .Federation}}
|
||||
<h3>Fediverse stats</h3>
|
||||
<table id="fediverse" class="classy export">
|
||||
<tr>
|
||||
<th>Followers</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{.APFollowers}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{end}}
|
||||
|
||||
<h3>Top {{len .TopPosts}} posts</h3>
|
||||
<table class="classy export">
|
||||
<tr>
|
||||
<th>Post</th>
|
||||
{{if not .Collection}}<th>Blog</th>{{end}}
|
||||
<th class="num">Total Views</th>
|
||||
</tr>
|
||||
{{range .TopPosts}}<tr>
|
||||
<td style="word-break: break-all;"><a href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}/{{.ID}}{{end}}">{{if ne .Title.String ""}}{{.Title.String}}{{else}}<em>{{.ID}}</em>{{end}}</a></td>
|
||||
{{ if not $.Collection }}<td>{{if .Collection}}<a href="{{.Collection.CanonicalURL}}">{{.Collection.Title}}</a>{{else}}<em>Draft</em>{{end}}</td>{{ end }}
|
||||
<td class="num">{{.ViewCount}}</td>
|
||||
</tr>{{end}}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
Loading…
Reference in a new issue