aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/handlers_design.go
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-12 12:26:17 +0300
committergrm <grm@eyesin.space>2026-09-12 12:26:17 +0300
commit2b450b66fee4906e746f6e04a6d07a72ff2faf64 (patch)
treeba2dbb3d9b3580113510dbf24d4ffe660cf95b26 /internal/web/handlers_design.go
parent336199c65d910bbd45a9c5dcbc3beae214d75b98 (diff)
downloadblogspace-2b450b66fee4906e746f6e04a6d07a72ff2faf64.tar.gz
blogspace-2b450b66fee4906e746f6e04a6d07a72ff2faf64.tar.bz2
blogspace-2b450b66fee4906e746f6e04a6d07a72ff2faf64.zip
Insert images straight from the Markdown editor
A shared editor partial adds an "Insert image" file picker plus paste and drag-and-drop on the textarea; a few lines of inline JS upload the file to the existing images endpoint (which now answers JSON when asked) and put the Markdown at the caret. Without JS the same file rides along with the form and is appended to the text on save, so nothing depends on scripting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/web/handlers_design.go')
-rw-r--r--internal/web/handlers_design.go47
1 files changed, 44 insertions, 3 deletions
diff --git a/internal/web/handlers_design.go b/internal/web/handlers_design.go
index 601ba34..0db66c2 100644
--- a/internal/web/handlers_design.go
+++ b/internal/web/handlers_design.go
@@ -2,12 +2,14 @@ package web
import (
"bytes"
+ "encoding/json"
"errors"
"io"
"mime/multipart"
"net/http"
"path/filepath"
"strconv"
+ "strings"
"github.com/google/uuid"
"github.com/gramanas/blogspace/internal/store"
@@ -89,6 +91,24 @@ func (s *Server) storeUpload(r *http.Request, fh *multipart.FileHeader) (*store.
return s.st.CreateImage(r.Context(), currentBlog(r).ID, name, ct, buf.Bytes())
}
+// imageMarkdown is the line the editor inserts for an uploaded image.
+func imageMarkdown(img *store.Image) string {
+ return "![" + strings.NewReplacer("]", "", "\n", " ").Replace(img.Filename) + "](/media/" + img.ID.String() + ")"
+}
+
+// appendImageMD is the no-JavaScript path of "Insert image": the file arrives
+// with the form itself and is appended to the end of the text on save.
+func appendImageMD(md string, img *store.Image) string {
+ if img == nil {
+ return md
+ }
+ md = strings.TrimRight(md, "\n")
+ if md != "" {
+ md += "\n\n"
+ }
+ return md + imageMarkdown(img) + "\n"
+}
+
func kbString(n int64) string {
if n >= 1<<20 {
return strconv.FormatInt(n>>20, 10) + " MB"
@@ -107,24 +127,45 @@ func (s *Server) handleImages(w http.ResponseWriter, r *http.Request) {
s.render(w, r, "dashboard/images.html", map[string]any{"images": images})
}
+// handleImageUpload serves the Images page form and, when the client asks for
+// JSON, the editor's "Insert image" script.
func (s *Server) handleImageUpload(w http.ResponseWriter, r *http.Request) {
blog := currentBlog(r)
+ wantJSON := strings.Contains(r.Header.Get("Accept"), "application/json")
+ fail := func(msg string) {
+ if wantJSON {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": msg})
+ return
+ }
+ s.plainError(w, http.StatusBadRequest, msg)
+ }
if err := r.ParseMultipartForm(1 << 20); err != nil {
- s.plainError(w, http.StatusBadRequest, "Upload too large or malformed form.")
+ fail("Upload too large or malformed form.")
return
}
img, err := s.readUpload(r, "file")
if err != nil {
- s.plainError(w, http.StatusBadRequest, err.Error())
+ fail(err.Error())
return
}
if img == nil {
- s.plainError(w, http.StatusBadRequest, "Choose a file first.")
+ fail("Choose a file first.")
+ return
+ }
+ if wantJSON {
+ writeJSON(w, http.StatusOK, map[string]string{"id": img.ID.String(), "filename": img.Filename, "markdown": imageMarkdown(img)})
return
}
redirectOK(w, r, "/b/"+blog.Subdomain+"/images", "Uploaded "+img.Filename+".")
}
+func writeJSON(w http.ResponseWriter, status int, v any) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("X-Content-Type-Options", "nosniff")
+ w.WriteHeader(status)
+ _ = json.NewEncoder(w).Encode(v)
+}
+
func (s *Server) handleImageDelete(w http.ResponseWriter, r *http.Request) {
blog := currentBlog(r)
id, err := uuid.Parse(r.PathValue("id"))