diff --git a/app.go b/app.go index 9e50d97..349ec1d 100644 --- a/app.go +++ b/app.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "os/signal" + "regexp" "syscall" "github.com/gorilla/mux" @@ -37,6 +38,25 @@ type app struct { sessionStore *sessions.CookieStore } +// handleViewHome shows page at root path. Will be the Pad if logged in and the +// catch-all landing page otherwise. +func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error { + if app.cfg.App.SingleUser { + // Render blog index + return handleViewCollection(app, w, r) + } + + // Multi-user instance + u := getUserSession(app, r) + if u != nil { + // User is logged in, so show the Pad + return handleViewPad(app, w, r) + } + + // Show landing page + return renderPage(w, "landing.tmpl", pageForReq(app, r)) +} + func pageForReq(app *app, r *http.Request) page.StaticPage { p := page.StaticPage{ AppCfg: app.cfg.App, @@ -67,6 +87,7 @@ func pageForReq(app *app, r *http.Request) page.StaticPage { } var shttp = http.NewServeMux() +var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$") func Serve() { debugPtr := flag.Bool("debug", false, "Enables debug logging.") diff --git a/pad.go b/pad.go new file mode 100644 index 0000000..6a04030 --- /dev/null +++ b/pad.go @@ -0,0 +1,144 @@ +package writefreely + +import ( + "github.com/gorilla/mux" + "github.com/writeas/impart" + "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/page" + "net/http" + "strings" +) + +func handleViewPad(app *app, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + action := vars["action"] + slug := vars["slug"] + collAlias := vars["collection"] + appData := &struct { + page.StaticPage + Post *RawPost + User *User + Blogs *[]Collection + + Editing bool // True if we're modifying an existing post + EditCollection *Collection // Collection of the post we're editing, if any + }{ + StaticPage: pageForReq(app, r), + Post: &RawPost{Font: "norm"}, + User: getUserSession(app, r), + } + var err error + if appData.User != nil { + appData.Blogs, err = app.db.GetPublishableCollections(appData.User) + if err != nil { + log.Error("Unable to get user's blogs for Pad: %v", err) + } + } + + padTmpl := "pad" + + if action == "" && slug == "" { + // Not editing any post; simply render the Pad + if err = templates[padTmpl].ExecuteTemplate(w, "pad", appData); err != nil { + log.Error("Unable to execute template: %v", err) + } + + return nil + } + + // Retrieve post information for editing + appData.Editing = true + // Make sure this isn't cached, so user doesn't accidentally lose data + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Expires", "Thu, 04 Oct 1990 20:00:00 GMT") + if slug != "" { + appData.Post = getRawCollectionPost(app, slug, collAlias) + if appData.Post.OwnerID != appData.User.ID { + // TODO: add ErrForbiddenEditPost message to flashes + return impart.HTTPError{http.StatusFound, r.URL.Path[:strings.LastIndex(r.URL.Path, "/edit")]} + } + appData.EditCollection, err = app.db.GetCollectionForPad(collAlias) + if err != nil { + return err + } + } else { + // Editing a floating article + appData.Post = getRawPost(app, action) + appData.Post.Id = action + } + + if appData.Post.Gone { + return ErrPostUnpublished + } else if appData.Post.Found && appData.Post.Content != "" { + // Got the post + } else if appData.Post.Found { + return ErrPostFetchError + } else { + return ErrPostNotFound + } + + if err = templates[padTmpl].ExecuteTemplate(w, "pad", appData); err != nil { + log.Error("Unable to execute template: %v", err) + } + return nil +} + +func handleViewMeta(app *app, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + action := vars["action"] + slug := vars["slug"] + collAlias := vars["collection"] + appData := &struct { + page.StaticPage + Post *RawPost + User *User + EditCollection *Collection // Collection of the post we're editing, if any + Flashes []string + NeedsToken bool + }{ + StaticPage: pageForReq(app, r), + Post: &RawPost{Font: "norm"}, + User: getUserSession(app, r), + } + var err error + + if action == "" && slug == "" { + return ErrPostNotFound + } + + // Make sure this isn't cached, so user doesn't accidentally lose data + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Expires", "Thu, 28 Jul 1989 12:00:00 GMT") + if slug != "" { + appData.Post = getRawCollectionPost(app, slug, collAlias) + if appData.Post.OwnerID != appData.User.ID { + // TODO: add ErrForbiddenEditPost message to flashes + return impart.HTTPError{http.StatusFound, r.URL.Path[:strings.LastIndex(r.URL.Path, "/meta")]} + } + appData.EditCollection, err = app.db.GetCollectionForPad(collAlias) + if err != nil { + return err + } + } else { + // Editing a floating article + appData.Post = getRawPost(app, action) + appData.Post.Id = action + } + appData.NeedsToken = appData.User == nil || appData.User.ID != appData.Post.OwnerID + + if appData.Post.Gone { + return ErrPostUnpublished + } else if appData.Post.Found && appData.Post.Content != "" { + // Got the post + } else if appData.Post.Found { + return ErrPostFetchError + } else { + return ErrPostNotFound + } + appData.Flashes, _ = getSessionFlashes(app, w, r, nil) + + if err = templates["edit-meta"].ExecuteTemplate(w, "edit-meta", appData); err != nil { + log.Error("Unable to execute template: %v", err) + } + return nil +} diff --git a/routes.go b/routes.go index ff84ca2..92462b6 100644 --- a/routes.go +++ b/routes.go @@ -45,6 +45,12 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto posts.HandleFunc("/claim", handler.All(addPost)).Methods("POST") posts.HandleFunc("/disperse", handler.All(dispersePost)).Methods("POST") + if cfg.App.SingleUser { + write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") + } else { + write.HandleFunc("/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") + } + // All the existing stuff write.HandleFunc("/{action}/edit", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") write.HandleFunc("/{action}/meta", handler.Web(handleViewMeta, UserLevelOptional)).Methods("GET") @@ -54,4 +60,5 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto // Posts write.HandleFunc("/{post}", handler.Web(handleViewPost, UserLevelOptional)) } + write.HandleFunc("/", handler.Web(handleViewHome, UserLevelOptional)) } diff --git a/static/img/ic_blogs@2x.png b/static/img/ic_blogs@2x.png new file mode 100644 index 0000000..b2ecd31 Binary files /dev/null and b/static/img/ic_blogs@2x.png differ diff --git a/static/img/ic_blogs_dark@2x.png b/static/img/ic_blogs_dark@2x.png new file mode 100644 index 0000000..4ebf9d7 Binary files /dev/null and b/static/img/ic_blogs_dark@2x.png differ diff --git a/static/img/ic_brightness@2x.png b/static/img/ic_brightness@2x.png new file mode 100644 index 0000000..9352818 Binary files /dev/null and b/static/img/ic_brightness@2x.png differ diff --git a/static/img/ic_brightness_dark@2x.png b/static/img/ic_brightness_dark@2x.png new file mode 100644 index 0000000..abef5bd Binary files /dev/null and b/static/img/ic_brightness_dark@2x.png differ diff --git a/static/img/ic_down_arrow@2x.png b/static/img/ic_down_arrow@2x.png new file mode 100644 index 0000000..bbb4fb4 Binary files /dev/null and b/static/img/ic_down_arrow@2x.png differ diff --git a/static/img/ic_down_arrow_dark@2x.png b/static/img/ic_down_arrow_dark@2x.png new file mode 100644 index 0000000..3d7f83f Binary files /dev/null and b/static/img/ic_down_arrow_dark@2x.png differ diff --git a/static/img/ic_edit@2x.png b/static/img/ic_edit@2x.png new file mode 100644 index 0000000..5a06bff Binary files /dev/null and b/static/img/ic_edit@2x.png differ diff --git a/static/img/ic_edit_dark@2x.png b/static/img/ic_edit_dark@2x.png new file mode 100644 index 0000000..87f8de1 Binary files /dev/null and b/static/img/ic_edit_dark@2x.png differ diff --git a/static/img/ic_font@2x.png b/static/img/ic_font@2x.png new file mode 100644 index 0000000..612d143 Binary files /dev/null and b/static/img/ic_font@2x.png differ diff --git a/static/img/ic_font_dark@2x.png b/static/img/ic_font_dark@2x.png new file mode 100644 index 0000000..44c3710 Binary files /dev/null and b/static/img/ic_font_dark@2x.png differ diff --git a/static/img/ic_info@2x.png b/static/img/ic_info@2x.png new file mode 100644 index 0000000..c571b2e Binary files /dev/null and b/static/img/ic_info@2x.png differ diff --git a/static/img/ic_info_dark@2x.png b/static/img/ic_info_dark@2x.png new file mode 100644 index 0000000..b706f0d Binary files /dev/null and b/static/img/ic_info_dark@2x.png differ diff --git a/static/img/ic_list@2x.png b/static/img/ic_list@2x.png new file mode 100644 index 0000000..b81d910 Binary files /dev/null and b/static/img/ic_list@2x.png differ diff --git a/static/img/ic_list_dark@2x.png b/static/img/ic_list_dark@2x.png new file mode 100644 index 0000000..73372f4 Binary files /dev/null and b/static/img/ic_list_dark@2x.png differ diff --git a/static/img/ic_send@2x.png b/static/img/ic_send@2x.png new file mode 100644 index 0000000..ef59e77 Binary files /dev/null and b/static/img/ic_send@2x.png differ diff --git a/static/img/ic_send_dark@2x.png b/static/img/ic_send_dark@2x.png new file mode 100644 index 0000000..625bed9 Binary files /dev/null and b/static/img/ic_send_dark@2x.png differ diff --git a/static/js/h.js b/static/js/h.js new file mode 100644 index 0000000..49720be --- /dev/null +++ b/static/js/h.js @@ -0,0 +1,257 @@ +/** + * H.js + * + * Lightweight, extremely bare-bones library for manipulating the DOM and + * saving some typing. + */ + +var Element = function(domElement) { + this.el = domElement; +}; + +/** + * Creates a toggle button that adds / removes the given class name from the + * given element. + * + * @param {Element} $el - The element to modify. + * @param {string} onClass - The class to add to the given element. + * @param {function} onFunc - Additional actions when toggling on. + * @param {function} offFunc - Additional actions when toggling off. + */ +Element.prototype.createToggle = function($el, onClass, onFunc, offFunc) { + this.on('click', function(e) { + if ($el.el.className === '') { + $el.el.className = onClass; + onFunc(new Element(this), e); + } else { + $el.el.className = ''; + offFunc(new Element(this), e); + } + e.preventDefault(); + }, false); +}; +Element.prototype.on = function(event, func) { + events = event.split(' '); + var el = this.el; + if (el == null) { + console.error("Error: element for event is null"); + return; + } + var addEvent = function(e) { + if (el.addEventListener) { + el.addEventListener(e, func, false); + } else if (el.attachEvent) { + el.attachEvent(e, func); + } + }; + if (events.length === 1) { + addEvent(event); + } else { + for(var i=0; i summaryLen) { + summary = summary.substring(0, summaryLen) + "..."; + } + return { + title: content.substring("# ".length, eol), + summary: summary, + }; + } + + var blankLine = content.indexOf("\n\n"); + if (blankLine !== -1 && blankLine <= eol && blankLine <= titleLen) { + // Title is in the format: + // + // Some title + // + // The body starts after that blank line above it. + var summary = content.substring(blankLine).trim(); + if (summary.length > summaryLen) { + summary = summary.substring(0, summaryLen) + "..."; + } + return { + title: content.substring(0, blankLine), + summary: summary, + }; + } + + // TODO: move this to the beginning + var title = content.trim(); + var summary = ""; + if (title.length > titleLen) { + // Content can't fit in the title, so figure out the summary + summary = title; + title = ""; + if (summary.length > summaryLen) { + summary = summary.substring(0, summaryLen) + "..."; + } + } else if (eol > 0) { + summary = title.substring(eol+1); + title = title.substring(0, eol); + } + return { + title: title, + summary: summary + }; + }; + + var post = getPostMeta(content); + post.id = id; + post.token = editToken; + post.created = created ? new Date(created) : new Date(); + post.client = "Pad"; + + return post; + }, + getTitleStrict: function(content) { + var eol = content.indexOf("\n"); + var title = ""; + var newContent = content; + if (content.indexOf("# ") === 0) { + // Title is in the format: + // # Some title + if (eol !== -1) { + // First line should start with # and end with \n + newContent = content.substring(eol).leftTrim(); + title = content.substring("# ".length, eol); + } + } + return { + title: title, + content: newContent + }; + }, +}; + +var He = { + create: function(name) { + return document.createElement(name); + }, + get: function(id) { + return document.getElementById(id); + }, + $: function(selector) { + var els = document.querySelectorAll(selector); + return els; + }, + postJSON: function(url, params, callback) { + var http = new XMLHttpRequest(); + + http.open("POST", url, true); + + // Send the proper header information along with the request + http.setRequestHeader("Content-type", "application/json"); + + http.onreadystatechange = function() { + if (http.readyState == 4) { + callback(http.status, JSON.parse(http.responseText)); + } + } + http.send(JSON.stringify(params)); + }, +}; + +String.prototype.leftTrim = function() { + return this.replace(/^\s+/,""); +}; diff --git a/templates/pad.tmpl b/templates/pad.tmpl new file mode 100644 index 0000000..cbce9bf --- /dev/null +++ b/templates/pad.tmpl @@ -0,0 +1,358 @@ +{{define "pad"}} + + + + {{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}} + + + + + + + + +
+ + + +
+
+

{{if .User}}{{else}}write.as{{end}} +

+ + + +
+ +
+ {{if .Editing}}{{end}} + +
+
+
+
+ + + + + +{{end}}