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/data" | ||||
"github.com/writeas/web-core/log" | "github.com/writeas/web-core/log" | ||||
"github.com/writeas/writefreely/author" | "github.com/writeas/writefreely/author" | ||||
"github.com/writeas/writefreely/config" | |||||
"github.com/writeas/writefreely/page" | "github.com/writeas/writefreely/page" | ||||
"html/template" | "html/template" | ||||
"net/http" | "net/http" | ||||
@@ -58,11 +59,15 @@ func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []str | |||||
up.Flashes = flashes | up.Flashes = flashes | ||||
up.Path = r.URL.Path | up.Path = r.URL.Path | ||||
up.IsAdmin = u.IsAdmin() | 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 | 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) { | func (up *UserPage) SetMessaging(u *User) { | ||||
//up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID) | //up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID) | ||||
} | } | ||||
@@ -317,6 +317,8 @@ func pageForReq(app *App, r *http.Request) page.StaticPage { | |||||
u = getUserSession(app, r) | u = getUserSession(app, r) | ||||
if u != nil { | if u != nil { | ||||
p.Username = u.Username | 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 | p.CanViewReader = !app.cfg.App.Private || u != nil | ||||
@@ -525,6 +525,8 @@ type CollectionPage struct { | |||||
Username string | Username string | ||||
Collections *[]Collection | Collections *[]Collection | ||||
PinnedPosts *[]PublicPost | PinnedPosts *[]PublicPost | ||||
IsAdmin bool | |||||
CanInvite bool | |||||
} | } | ||||
func (c *CollectionObj) ScriptDisplay() template.JS { | func (c *CollectionObj) ScriptDisplay() template.JS { | ||||
@@ -737,6 +739,8 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro | |||||
IsCustomDomain: cr.isCustomDomain, | IsCustomDomain: cr.isCustomDomain, | ||||
IsWelcome: r.FormValue("greeting") != "", | IsWelcome: r.FormValue("greeting") != "", | ||||
} | } | ||||
displayPage.IsAdmin = u != nil && u.IsAdmin() | |||||
displayPage.CanInvite = canUserInvite(app.cfg, displayPage.IsAdmin) | |||||
var owner *User | var owner *User | ||||
if u != nil { | if u != nil { | ||||
displayPage.Username = u.Username | 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 | // TODO: fix this mess of collections inside collections | ||||
displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj) | 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 { | if err != nil { | ||||
log.Error("Unable to render collection index: %v", err) | log.Error("Unable to render collection index: %v", err) | ||||
} | } | ||||
@@ -69,6 +69,7 @@ type ( | |||||
WebFonts bool `ini:"webfonts"` | WebFonts bool `ini:"webfonts"` | ||||
Landing string `ini:"landing"` | Landing string `ini:"landing"` | ||||
SimpleNav bool `ini:"simple_nav"` | SimpleNav bool `ini:"simple_nav"` | ||||
Chorus bool `ini:"chorus"` | |||||
// Users | // Users | ||||
SingleUser bool `ini:"single_user"` | SingleUser bool `ini:"single_user"` | ||||
@@ -28,6 +28,8 @@ type StaticPage struct { | |||||
Values map[string]string | Values map[string]string | ||||
Flashes []string | Flashes []string | ||||
CanViewReader bool | CanViewReader bool | ||||
IsAdmin bool | |||||
CanInvite bool | |||||
} | } | ||||
// SanitizeHost alters the StaticPage to contain a real hostname. This is | // 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 | IsPinned bool | ||||
IsCustomDomain bool | IsCustomDomain bool | ||||
PinnedPosts *[]PublicPost | PinnedPosts *[]PublicPost | ||||
IsAdmin bool | |||||
CanInvite bool | |||||
}{ | }{ | ||||
PublicPost: p, | PublicPost: p, | ||||
StaticPage: pageForReq(app, r), | StaticPage: pageForReq(app, r), | ||||
IsOwner: cr.isCollOwner, | IsOwner: cr.isCollOwner, | ||||
IsCustomDomain: cr.isCustomDomain, | IsCustomDomain: cr.isCustomDomain, | ||||
} | } | ||||
tp.IsAdmin = u != nil && u.IsAdmin() | |||||
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) | |||||
tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll) | tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll) | ||||
tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p) | 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) | log.Error("Error in collection-post template: %v", err) | ||||
} | } | ||||
} | } | ||||
@@ -48,6 +48,8 @@ type readPublication struct { | |||||
CurrentPage int | CurrentPage int | ||||
TotalPages int | TotalPages int | ||||
SelTopic string | SelTopic string | ||||
IsAdmin bool | |||||
CanInvite bool | |||||
} | } | ||||
func initLocalTimeline(app *App) { | func initLocalTimeline(app *App) { | ||||
@@ -198,11 +200,16 @@ func showLocalTimeline(app *App, w http.ResponseWriter, r *http.Request, page in | |||||
} | } | ||||
d := &readPublication{ | 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) | 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, "include", "footer.tmpl"), | ||||
filepath.Join(parentDir, templatesDir, "base.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" | // These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl" | ||||
files = append(files, filepath.Join(parentDir, 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")) | files = append(files, filepath.Join(parentDir, templatesDir, "include", "post-render.tmpl")) | ||||
} | } | ||||
templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...)) | templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...)) | ||||
@@ -13,14 +13,38 @@ | |||||
<body {{template "body-attrs" .}}> | <body {{template "body-attrs" .}}> | ||||
<div id="overlay"></div> | <div id="overlay"></div> | ||||
<header> | <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}} | {{if not .SingleUser}} | ||||
<nav id="user-nav"> | <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"> | <nav class="tabs"> | ||||
<a href="/about"{{if eq .Path "/about"}} class="selected"{{end}}>About</a> | <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 (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}} | {{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> | </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> | </nav> | ||||
{{end}} | {{end}} | ||||
</header> | </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 { | body#collection header nav { | ||||
display: inline !important; | display: inline !important; | ||||
} | |||||
body#collection header nav:not(#full-nav):not(#user-nav) { | |||||
margin: 0 0 0 1em !important; | margin: 0 0 0 1em !important; | ||||
} | } | ||||
header nav#user-nav { | header nav#user-nav { | ||||
margin-left: 0 !important; | margin-left: 0 !important; | ||||
} | } | ||||
body#collection header nav.tabs a:first-child { | |||||
margin-left: 1em; | |||||
} | |||||
</style> | </style> | ||||
{{end}} | {{end}} | ||||
{{define "body-attrs"}}id="collection"{{end}} | {{define "body-attrs"}}id="collection"{{end}} | ||||
@@ -1,5 +1,5 @@ | |||||
{{define "user-navigation"}} | {{define "user-navigation"}} | ||||
<header{{if .SingleUser}} class="singleuser"{{end}}> | |||||
<header class="{{if .SingleUser}}singleuser{{else}}multiuser{{end}}"> | |||||
{{if .SingleUser}} | {{if .SingleUser}} | ||||
<nav id="user-nav"> | <nav id="user-nav"> | ||||
<nav class="dropdown-nav"> | <nav class="dropdown-nav"> | ||||
@@ -30,6 +30,7 @@ | |||||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1> | <h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1> | ||||
{{ end }} | {{ end }} | ||||
<nav id="user-nav"> | <nav id="user-nav"> | ||||
{{if .Username}} | |||||
<nav class="dropdown-nav"> | <nav class="dropdown-nav"> | ||||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul> | <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}} | {{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}} | ||||
@@ -41,20 +42,21 @@ | |||||
</ul></li> | </ul></li> | ||||
</ul> | </ul> | ||||
</nav> | </nav> | ||||
{{end}} | |||||
<nav class="tabs"> | <nav class="tabs"> | ||||
{{if .SimpleNav}} | {{if .SimpleNav}} | ||||
<a href="/about">About</a> | <a href="/about">About</a> | ||||
{{if and (and (not .SingleUser) .LocalTimeline) .CanViewReader}}<a href="/read">Reader</a>{{end}} | {{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}} | {{else}} | ||||
<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a> | <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> | <a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a> | ||||
{{end}} | {{end}} | ||||
</nav> | </nav> | ||||
</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> | <a class="simple-btn" href="/new">New Post</a> | ||||
</div> | |||||
</div>{{end}} | |||||
</nav> | </nav> | ||||
{{end}} | {{end}} | ||||
{{end}} | {{end}} | ||||