This adds a new config value: `chorus` that signifies an instance is more about the Reader view than individual blogs / writers. When enabled, user navigation will show on all pages, including About, Reader, and Privacy (ref T680). It also uses different collection templates that keep the instance-wide navigation at the top of the page, instead of the author's name -- again, branded more for the collective than the individual. Ref T681customize-reader
@@ -21,6 +21,7 @@ import ( | |||
"github.com/writeas/web-core/data" | |||
"github.com/writeas/web-core/log" | |||
"github.com/writeas/writefreely/author" | |||
"github.com/writeas/writefreely/config" | |||
"github.com/writeas/writefreely/page" | |||
"html/template" | |||
"net/http" | |||
@@ -58,11 +59,15 @@ func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []str | |||
up.Flashes = flashes | |||
up.Path = r.URL.Path | |||
up.IsAdmin = u.IsAdmin() | |||
up.CanInvite = app.cfg.App.UserInvites != "" && | |||
(up.IsAdmin || app.cfg.App.UserInvites != "admin") | |||
up.CanInvite = canUserInvite(app.cfg, up.IsAdmin) | |||
return up | |||
} | |||
func canUserInvite(cfg *config.Config, isAdmin bool) bool { | |||
return cfg.App.UserInvites != "" && | |||
(isAdmin || cfg.App.UserInvites != "admin") | |||
} | |||
func (up *UserPage) SetMessaging(u *User) { | |||
//up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID) | |||
} | |||
@@ -317,6 +317,8 @@ func pageForReq(app *App, r *http.Request) page.StaticPage { | |||
u = getUserSession(app, r) | |||
if u != nil { | |||
p.Username = u.Username | |||
p.IsAdmin = u != nil && u.IsAdmin() | |||
p.CanInvite = canUserInvite(app.cfg, p.IsAdmin) | |||
} | |||
} | |||
p.CanViewReader = !app.cfg.App.Private || u != nil | |||
@@ -525,6 +525,8 @@ type CollectionPage struct { | |||
Username string | |||
Collections *[]Collection | |||
PinnedPosts *[]PublicPost | |||
IsAdmin bool | |||
CanInvite bool | |||
} | |||
func (c *CollectionObj) ScriptDisplay() template.JS { | |||
@@ -737,6 +739,8 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro | |||
IsCustomDomain: cr.isCustomDomain, | |||
IsWelcome: r.FormValue("greeting") != "", | |||
} | |||
displayPage.IsAdmin = u != nil && u.IsAdmin() | |||
displayPage.CanInvite = canUserInvite(app.cfg, displayPage.IsAdmin) | |||
var owner *User | |||
if u != nil { | |||
displayPage.Username = u.Username | |||
@@ -768,7 +772,11 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro | |||
// TODO: fix this mess of collections inside collections | |||
displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj) | |||
err = templates["collection"].ExecuteTemplate(w, "collection", displayPage) | |||
collTmpl := "collection" | |||
if app.cfg.App.Chorus { | |||
collTmpl = "chorus-collection" | |||
} | |||
err = templates[collTmpl].ExecuteTemplate(w, "collection", displayPage) | |||
if err != nil { | |||
log.Error("Unable to render collection index: %v", err) | |||
} | |||
@@ -69,6 +69,7 @@ type ( | |||
WebFonts bool `ini:"webfonts"` | |||
Landing string `ini:"landing"` | |||
SimpleNav bool `ini:"simple_nav"` | |||
Chorus bool `ini:"chorus"` | |||
// Users | |||
SingleUser bool `ini:"single_user"` | |||
@@ -28,6 +28,8 @@ type StaticPage struct { | |||
Values map[string]string | |||
Flashes []string | |||
CanViewReader bool | |||
IsAdmin bool | |||
CanInvite bool | |||
} | |||
// SanitizeHost alters the StaticPage to contain a real hostname. This is | |||
@@ -1345,15 +1345,24 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error | |||
IsPinned bool | |||
IsCustomDomain bool | |||
PinnedPosts *[]PublicPost | |||
IsAdmin bool | |||
CanInvite bool | |||
}{ | |||
PublicPost: p, | |||
StaticPage: pageForReq(app, r), | |||
IsOwner: cr.isCollOwner, | |||
IsCustomDomain: cr.isCustomDomain, | |||
} | |||
tp.IsAdmin = u != nil && u.IsAdmin() | |||
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) | |||
tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll) | |||
tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p) | |||
if err := templates["collection-post"].ExecuteTemplate(w, "post", tp); err != nil { | |||
postTmpl := "collection-post" | |||
if app.cfg.App.Chorus { | |||
postTmpl = "chorus-collection-post" | |||
} | |||
if err := templates[postTmpl].ExecuteTemplate(w, "post", tp); err != nil { | |||
log.Error("Error in collection-post template: %v", err) | |||
} | |||
} | |||
@@ -48,6 +48,8 @@ type readPublication struct { | |||
CurrentPage int | |||
TotalPages int | |||
SelTopic string | |||
IsAdmin bool | |||
CanInvite bool | |||
} | |||
func initLocalTimeline(app *App) { | |||
@@ -198,11 +200,16 @@ func showLocalTimeline(app *App, w http.ResponseWriter, r *http.Request, page in | |||
} | |||
d := &readPublication{ | |||
pageForReq(app, r), | |||
&posts, | |||
page, | |||
ttlPages, | |||
tag, | |||
StaticPage: pageForReq(app, r), | |||
Posts: &posts, | |||
CurrentPage: page, | |||
TotalPages: ttlPages, | |||
SelTopic: tag, | |||
} | |||
if app.cfg.App.Chorus { | |||
u := getUserSession(app, r) | |||
d.IsAdmin = u != nil && u.IsAdmin() | |||
d.CanInvite = canUserInvite(app.cfg, d.IsAdmin) | |||
} | |||
err := templates["read"].ExecuteTemplate(w, "base", d) | |||
@@ -64,11 +64,14 @@ func initTemplate(parentDir, name string) { | |||
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), | |||
filepath.Join(parentDir, templatesDir, "base.tmpl"), | |||
} | |||
if name == "collection" || name == "collection-tags" { | |||
if name == "collection" || name == "collection-tags" || name == "chorus-collection" { | |||
// These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl" | |||
files = append(files, filepath.Join(parentDir, templatesDir, "include", "posts.tmpl")) | |||
} | |||
if name == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" { | |||
if name == "chorus-collection" || name == "chorus-collection-post" { | |||
files = append(files, filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl")) | |||
} | |||
if name == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" || name == "chorus-collection" || name == "chorus-collection-post" { | |||
files = append(files, filepath.Join(parentDir, templatesDir, "include", "post-render.tmpl")) | |||
} | |||
templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...)) | |||
@@ -13,14 +13,38 @@ | |||
<body {{template "body-attrs" .}}> | |||
<div id="overlay"></div> | |||
<header> | |||
<h2><a href="/">{{.SiteName}}</a></h2> | |||
{{ if .SimpleNav }}<nav id="full-nav"> | |||
<div class="left-side"> | |||
<h2><a href="/">{{.SiteName}}</a></h2> | |||
</div> | |||
{{ else }} | |||
<h2><a href="/">{{.SiteName}}</a></h2> | |||
{{ end }} | |||
{{if not .SingleUser}} | |||
<nav id="user-nav"> | |||
{{if and .Chorus .Username}} | |||
<nav class="dropdown-nav"> | |||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul> | |||
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}} | |||
<li><a href="/me/settings">Account settings</a></li> | |||
<li><a href="/me/export">Export</a></li> | |||
{{if .CanInvite}}<li><a href="/me/invites">Invite people</a></li>{{end}} | |||
<li class="separator"><hr /></li> | |||
<li><a href="/me/logout">Log out</a></li> | |||
</ul></li> | |||
</ul> | |||
</nav> | |||
{{end}} | |||
<nav class="tabs"> | |||
<a href="/about"{{if eq .Path "/about"}} class="selected"{{end}}>About</a> | |||
{{if and (and (not .SingleUser) .LocalTimeline) .CanViewReader}}<a href="/read"{{if eq .Path "/read"}} class="selected"{{end}}>Reader</a>{{end}} | |||
{{if and (not .SingleUser) (not .Username)}}<a href="/login"{{if eq .Path "/login"}} class="selected"{{end}}>Log in</a>{{else if .SimpleNav}}<a href="/me/logout">Log out</a>{{end}} | |||
</nav> | |||
{{if .SimpleNav}}{{if .Username}}<div class="right-side" style="font-size: 0.86em;"> | |||
<a class="simple-btn" href="/new">New Post</a> | |||
</div>{{end}} | |||
</nav> | |||
{{end}} | |||
</nav> | |||
{{end}} | |||
</header> | |||
@@ -0,0 +1,150 @@ | |||
{{define "post"}}<!DOCTYPE HTML> | |||
<html {{if .Language.Valid}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}"> | |||
<head prefix="og: http://ogp.me/ns# article: http://ogp.me/ns/article#"> | |||
<meta charset="utf-8"> | |||
<title>{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{.Collection.DisplayTitle}}</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" /> | |||
<link rel="canonical" href="{{.CanonicalURL}}" /> | |||
<meta name="generator" content="WriteFreely"> | |||
<meta name="title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}"> | |||
<meta name="description" content="{{.Summary}}"> | |||
{{if gt .Views 1}}<meta name="twitter:label1" value="Views"> | |||
<meta name="twitter:data1" value="{{largeNumFmt .Views}}">{{end}} | |||
<meta name="author" content="{{.Collection.Title}}" /> | |||
<meta itemprop="description" content="{{.Summary}}"> | |||
<meta itemprop="datePublished" content="{{.CreatedDate}}" /> | |||
<meta name="twitter:card" content="summary"> | |||
<meta name="twitter:description" content="{{.Summary}}"> | |||
<meta name="twitter:title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}"> | |||
{{if gt (len .Images) 0}}<meta name="twitter:image" content="{{index .Images 0}}">{{else}}<meta name="twitter:image" content="{{.Collection.AvatarURL}}">{{end}} | |||
<meta property="og:title" content="{{.PlainDisplayTitle}}" /> | |||
<meta property="og:description" content="{{.Summary}}" /> | |||
<meta property="og:site_name" content="{{.Collection.DisplayTitle}}" /> | |||
<meta property="og:type" content="article" /> | |||
<meta property="og:url" content="{{.CanonicalURL}}" /> | |||
<meta property="og:updated_time" content="{{.Created8601}}" /> | |||
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}} | |||
<meta property="article:published_time" content="{{.Created8601}}"> | |||
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}} | |||
<style type="text/css"> | |||
body footer { | |||
max-width: 40rem; | |||
margin: 0 auto; | |||
} | |||
body#post header { | |||
padding: 1em 1rem; | |||
} | |||
article time.dt-published { | |||
display: block; | |||
color: #666; | |||
} | |||
body#post article h2#title{ | |||
margin-bottom: 0.5em; | |||
} | |||
article time.dt-published { | |||
margin-bottom: 1em; | |||
} | |||
</style> | |||
{{if .Collection.RenderMathJax}} | |||
<!-- Add mathjax logic --> | |||
{{template "mathjax" . }} | |||
{{end}} | |||
<!-- Add highlighting logic --> | |||
{{template "highlighting" .}} | |||
</head> | |||
<body id="post"> | |||
<div id="overlay"></div> | |||
{{template "user-navigation" .}} | |||
<article id="post-body" class="{{.Font}} h-entry">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}{{/* TODO: check format: if .Collection.Format.ShowDates*/}}<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time><div class="e-content">{{.HTMLContent}}</div></article> | |||
{{ if .Collection.ShowFooterBranding }} | |||
<footer dir="ltr"> | |||
<p style="text-align: left">Published by <a rel="author" href="{{if .IsTopLevel}}/{{else}}/{{.Collection.Alias}}/{{end}}" class="h-card p-author">{{.Collection.DisplayTitle}}</a> | |||
{{ if .IsOwner }} · <span class="views" dir="ltr"><strong>{{largeNumFmt .Views}}</strong> {{pluralize "view" "views" .Views}}</span> | |||
· <a class="xtra-feature" href="/{{if not .SingleUser}}{{.Collection.Alias}}/{{end}}{{.Slug.String}}/edit" dir="{{.Direction}}">Edit</a> | |||
{{if .IsPinned}} · <a class="xtra-feature unpin" href="/{{.Collection.Alias}}/{{.Slug.String}}/unpin" dir="{{.Direction}}" onclick="unpinPost(event, '{{.ID}}')">Unpin</a>{{end}} | |||
{{ end }} | |||
</p> | |||
<nav> | |||
{{if .PinnedPosts}} | |||
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}} | |||
{{end}} | |||
</nav> | |||
<hr> | |||
<nav><p style="font-size: 0.9em">{{localhtml "published with write.as" .Language.String}}</p></nav> | |||
</footer> | |||
{{ end }} | |||
</body> | |||
{{if .Collection.CanShowScript}} | |||
{{range .Collection.ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}} | |||
{{if .Collection.Script}}<script type="text/javascript">{{.Collection.ScriptDisplay}}</script>{{end}} | |||
{{end}} | |||
<script type="text/javascript"> | |||
var pinning = false; | |||
function unpinPost(e, postID) { | |||
e.preventDefault(); | |||
if (pinning) { | |||
return; | |||
} | |||
pinning = true; | |||
var $header = document.getElementsByTagName('header')[0]; | |||
var callback = function() { | |||
// Hide current page | |||
var $pinnedNavLink = $header.getElementsByTagName('nav')[0].querySelector('.pinned.selected'); | |||
$pinnedNavLink.style.display = 'none'; | |||
}; | |||
var $pinBtn = $header.getElementsByClassName('unpin')[0]; | |||
$pinBtn.innerHTML = '...'; | |||
var http = new XMLHttpRequest(); | |||
var url = "/api/collections/{{.Collection.Alias}}/unpin"; | |||
var params = [ { "id": postID } ]; | |||
http.open("POST", url, true); | |||
http.setRequestHeader("Content-type", "application/json"); | |||
http.onreadystatechange = function() { | |||
if (http.readyState == 4) { | |||
pinning = false; | |||
if (http.status == 200) { | |||
callback(); | |||
$pinBtn.style.display = 'none'; | |||
$pinBtn.innerHTML = 'Pin'; | |||
} else if (http.status == 409) { | |||
$pinBtn.innerHTML = 'Unpin'; | |||
} else { | |||
$pinBtn.innerHTML = 'Unpin'; | |||
alert("Failed to unpin." + (http.status>=500?" Please try again.":"")); | |||
} | |||
} | |||
} | |||
http.send(JSON.stringify(params)); | |||
}; | |||
try { // Fonts | |||
WebFontConfig = { | |||
custom: { families: [ 'Lora:400,700:latin', 'Open+Sans: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> | |||
</html>{{end}} |
@@ -0,0 +1,230 @@ | |||
{{define "collection"}}<!DOCTYPE HTML> | |||
<html {{if .Language}}lang="{{.Language}}"{{end}} dir="{{.Direction}}"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<title>{{.DisplayTitle}}{{if not .SingleUser}} — {{.SiteName}}{{end}}</title> | |||
<link rel="stylesheet" type="text/css" href="/css/write.css" /> | |||
<link rel="shortcut icon" href="/favicon.ico" /> | |||
<link rel="canonical" href="{{.CanonicalURL}}"> | |||
{{if gt .CurrentPage 1}}<link rel="prev" href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">{{end}} | |||
{{if lt .CurrentPage .TotalPages}}<link rel="next" href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">{{end}} | |||
{{if not .IsPrivate}}<link rel="alternate" type="application/rss+xml" title="{{.DisplayTitle}} » Feed" href="{{.CanonicalURL}}feed/" />{{end}} | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
<meta name="generator" content="WriteFreely"> | |||
<meta name="description" content="{{.Description}}"> | |||
<meta itemprop="name" content="{{.DisplayTitle}}"> | |||
<meta itemprop="description" content="{{.Description}}"> | |||
<meta name="twitter:card" content="summary"> | |||
<meta name="twitter:title" content="{{.DisplayTitle}}"> | |||
<meta name="twitter:image" content="{{.AvatarURL}}"> | |||
<meta name="twitter:description" content="{{.Description}}"> | |||
<meta property="og:title" content="{{.DisplayTitle}}" /> | |||
<meta property="og:site_name" content="{{.DisplayTitle}}" /> | |||
<meta property="og:type" content="article" /> | |||
<meta property="og:url" content="{{.CanonicalURL}}" /> | |||
<meta property="og:description" content="{{.Description}}" /> | |||
<meta property="og:image" content="{{.AvatarURL}}"> | |||
{{if .StyleSheet}}<style type="text/css">{{.StyleSheetDisplay}}</style>{{end}} | |||
<style type="text/css"> | |||
body#collection header { | |||
max-width: 40em; | |||
margin: 1em auto; | |||
text-align: left; | |||
padding: 0; | |||
} | |||
body#collection header.multiuser { | |||
max-width: 100%; | |||
margin: 1em; | |||
} | |||
body#collection header nav:not(.pinned-posts) { | |||
display: inline; | |||
} | |||
body#collection header nav.dropdown-nav, | |||
body#collection header nav.tabs, | |||
body#collection header nav.tabs a:first-child { | |||
margin: 0 0 0 1em; | |||
} | |||
</style> | |||
{{if .RenderMathJax}} | |||
<!-- Add mathjax logic --> | |||
{{template "mathjax" .}} | |||
{{end}} | |||
<!-- Add highlighting logic --> | |||
{{template "highlighting" . }} | |||
</head> | |||
<body id="collection" itemscope itemtype="http://schema.org/WebPage"> | |||
{{template "user-navigation" .}} | |||
<header> | |||
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1> | |||
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}} | |||
{{/*if not .Public/*}} | |||
<!--p class="meta-note"><span>Private collection</span>. Only you can see this page.</p--> | |||
{{/*end*/}} | |||
{{if .PinnedPosts}}<nav class="pinned-posts"> | |||
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav> | |||
{{end}} | |||
</header> | |||
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}} | |||
{{if .IsWelcome}} | |||
<div id="welcome"> | |||
<h2>Welcome, <strong>{{.Username}}</strong>!</h2> | |||
<p>This is your new blog.</p> | |||
<p><a class="simple-cta" href="/#{{.Alias}}">Start writing</a>, or <a class="simple-cta" href="/me/c/{{.Alias}}">customize</a> your blog.</p> | |||
<p>Check out our <a class="simple-cta" href="https://guides.write.as/writing/?pk_campaign=welcome">writing guide</a> to see what else you can do, and <a class="simple-cta" href="/contact">get in touch</a> anytime with questions or feedback.</p> | |||
</div> | |||
{{end}} | |||
{{template "posts" .}} | |||
{{if gt .TotalPages 1}}<nav id="paging" class="content-container clearfix"> | |||
{{if or (and .Format.Ascending (lt .CurrentPage .TotalPages)) (isRTL .Direction)}} | |||
{{if gt .CurrentPage 1}}<a href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">⇠ {{if and .Format.Ascending (lt .CurrentPage .TotalPages)}}Previous{{else}}Newer{{end}}</a>{{end}} | |||
{{if lt .CurrentPage .TotalPages}}<a style="float:right;" href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">{{if and .Format.Ascending (lt .CurrentPage .TotalPages)}}Next{{else}}Older{{end}} ⇢</a>{{end}} | |||
{{else}} | |||
{{if lt .CurrentPage .TotalPages}}<a href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">⇠ Older</a>{{end}} | |||
{{if gt .CurrentPage 1}}<a style="float:right;" href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">Newer ⇢</a>{{end}} | |||
{{end}} | |||
</nav>{{end}} | |||
{{if .Posts}}</section>{{else}}</div>{{end}} | |||
{{if .ShowFooterBranding }} | |||
<footer> | |||
<hr /> | |||
<nav dir="ltr"> | |||
{{if not .SingleUser}}<a class="home pubd" href="/">{{.SiteName}}</a> · {{end}}powered by <a style="margin-left:0" href="https://writefreely.org">writefreely</a> | |||
</nav> | |||
</footer> | |||
{{ end }} | |||
</body> | |||
{{if .CanShowScript}} | |||
{{range .ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}} | |||
{{if .Script}}<script type="text/javascript">{{.ScriptDisplay}}</script>{{end}} | |||
{{end}} | |||
<script src="/js/h.js"></script> | |||
<script src="/js/postactions.js"></script> | |||
<script type="text/javascript"> | |||
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?')) { | |||
// AJAX | |||
deletePost(id, "", function() { | |||
// Remove post from list | |||
var $postEl = document.getElementById('post-' + id); | |||
$postEl.parentNode.removeChild($postEl); | |||
// TODO: add next post from this collection at the bottom | |||
}); | |||
} | |||
} | |||
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; | |||
http.open("DELETE", url, true); | |||
http.onreadystatechange = function() { | |||
if (http.readyState == 4) { | |||
deleting = false; | |||
if (http.status == 204) { | |||
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." + (http.status>=500?" Please try again.":"")); | |||
} | |||
} | |||
} | |||
http.send(); | |||
}; | |||
var pinning = false; | |||
function pinPost(e, postID, slug, title) { | |||
e.preventDefault(); | |||
if (pinning) { | |||
return; | |||
} | |||
pinning = true; | |||
var callback = function() { | |||
// Visibly remove post from collection | |||
var $postEl = document.getElementById('post-' + postID); | |||
$postEl.parentNode.removeChild($postEl); | |||
var $header = document.getElementsByTagName('header')[0]; | |||
var $pinnedNavs = $header.getElementsByTagName('nav'); | |||
// Add link to nav | |||
var link = '<a class="pinned" href="/{{.Alias}}/'+slug+'">'+title+'</a>'; | |||
if ($pinnedNavs.length == 0) { | |||
$header.insertAdjacentHTML("beforeend", '<nav>'+link+'</nav>'); | |||
} else { | |||
$pinnedNavs[0].insertAdjacentHTML("beforeend", link); | |||
} | |||
}; | |||
var $pinBtn = document.getElementById('post-' + postID).getElementsByClassName('pin action')[0]; | |||
$pinBtn.innerHTML = '...'; | |||
var http = new XMLHttpRequest(); | |||
var url = "/api/collections/{{.Alias}}/pin"; | |||
var params = [ { "id": postID } ]; | |||
http.open("POST", url, true); | |||
http.setRequestHeader("Content-type", "application/json"); | |||
http.onreadystatechange = function() { | |||
if (http.readyState == 4) { | |||
pinning = false; | |||
if (http.status == 200) { | |||
callback(); | |||
} else if (http.status == 409) { | |||
$pinBtn.innerHTML = 'pin'; | |||
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 { | |||
$pinBtn.innerHTML = 'pin'; | |||
alert("Failed to pin." + (http.status>=500?" Please try again.":"")); | |||
} | |||
} | |||
} | |||
http.send(JSON.stringify(params)); | |||
}; | |||
try { | |||
WebFontConfig = { | |||
custom: { families: [ 'Lora:400,700:latin', 'Open+Sans: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> | |||
</html>{{end}} |
@@ -65,11 +65,16 @@ | |||
} | |||
body#collection header nav { | |||
display: inline !important; | |||
} | |||
body#collection header nav:not(#full-nav):not(#user-nav) { | |||
margin: 0 0 0 1em !important; | |||
} | |||
header nav#user-nav { | |||
margin-left: 0 !important; | |||
} | |||
body#collection header nav.tabs a:first-child { | |||
margin-left: 1em; | |||
} | |||
</style> | |||
{{end}} | |||
{{define "body-attrs"}}id="collection"{{end}} | |||
@@ -1,5 +1,5 @@ | |||
{{define "user-navigation"}} | |||
<header{{if .SingleUser}} class="singleuser"{{end}}> | |||
<header class="{{if .SingleUser}}singleuser{{else}}multiuser{{end}}"> | |||
{{if .SingleUser}} | |||
<nav id="user-nav"> | |||
<nav class="dropdown-nav"> | |||
@@ -30,6 +30,7 @@ | |||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1> | |||
{{ end }} | |||
<nav id="user-nav"> | |||
{{if .Username}} | |||
<nav class="dropdown-nav"> | |||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul> | |||
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}} | |||
@@ -41,20 +42,21 @@ | |||
</ul></li> | |||
</ul> | |||
</nav> | |||
{{end}} | |||
<nav class="tabs"> | |||
{{if .SimpleNav}} | |||
<a href="/about">About</a> | |||
{{if and (and (not .SingleUser) .LocalTimeline) .CanViewReader}}<a href="/read">Reader</a>{{end}} | |||
<a href="/me/logout">Log out</a> | |||
{{if .Username}}<a href="/me/logout">Log out</a>{{else}}<a href="/login">Log in</a>{{end}} | |||
{{else}} | |||
<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> | |||
{{end}} | |||
</nav> | |||
</nav> | |||
{{if .SimpleNav}}<div class="right-side"> | |||
{{if .SimpleNav}}{{if .Username}}<div class="right-side"> | |||
<a class="simple-btn" href="/new">New Post</a> | |||
</div> | |||
</div>{{end}} | |||
</nav> | |||
{{end}} | |||
{{end}} | |||